Skip to content

Commit

Permalink
Implement zsh and fish completion generator
Browse files Browse the repository at this point in the history
Fixes #557
  • Loading branch information
dominikschulz committed Dec 25, 2017
1 parent 01edf0f commit 7f28502
Show file tree
Hide file tree
Showing 13 changed files with 389 additions and 114 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,3 @@ releases/
dist/

*.completion
!fish.completion
14 changes: 10 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ GOPASS_VERSION ?= $(shell cat VERSION)
GOPASS_OUTPUT ?= gopass
GOPASS_REVISION := $(shell cat COMMIT 2>/dev/null || git rev-parse --short=8 HEAD)
BASH_COMPLETION_OUTPUT := bash.completion
FISH_COMPLETION_OUTPUT := fish.completion
ZSH_COMPLETION_OUTPUT := zsh.completion
# Support reproducible builds by embedding date according to SOURCE_DATE_EPOCH if present
DATE := $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" '+%FT%T%z' 2>/dev/null || date -u '+%FT%T%z')
Expand All @@ -24,7 +25,7 @@ all: sysinfo crosscompile build install test codequality completion

define ok
@tput setaf 6 2>/dev/null || echo -n ""
@echo "[OK]"
@echo " [OK]"
@tput sgr0 2>/dev/null || echo -n ""
endef

Expand Down Expand Up @@ -98,16 +99,21 @@ crosscompile:
@GOOS=windows GOARCH=amd64 $(GO) build -o $(GOPASS_OUTPUT)-windows-amd64
@$(call ok)

completion: $(BASH_COMPLETION_OUTPUT) $(ZSH_COMPLETION_OUTPUT)
completion: $(BASH_COMPLETION_OUTPUT) $(FISH_COMPLETION_OUTPUT) $(ZSH_COMPLETION_OUTPUT)

$(BASH_COMPLETION_OUTPUT): build
@echo -n ">> BASH COMPLETION, output = $(BASH_COMPLETION_OUTPUT)"
@gopass completion bash > $(BASH_COMPLETION_OUTPUT)
@./gopass completion bash > $(BASH_COMPLETION_OUTPUT)
@$(call ok)

$(FISH_COMPLETION_OUTPUT): build
@echo -n ">> FISH COMPLETION, output = $(FISH_COMPLETION_OUTPUT)"
@./gopass completion fish > $(FISH_COMPLETION_OUTPUT)
@$(call ok)

$(ZSH_COMPLETION_OUTPUT): build
@echo -n ">> ZSH COMPLETION, output = $(ZSH_COMPLETION_OUTPUT)"
@gopass completion bash > $(ZSH_COMPLETION_OUTPUT)
@./gopass completion zsh > $(ZSH_COMPLETION_OUTPUT)
@$(call ok)

codequality:
Expand Down
26 changes: 19 additions & 7 deletions action/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"fmt"
"regexp"

fishcomp "github.com/justwatchcom/gopass/utils/completion/fish"
zshcomp "github.com/justwatchcom/gopass/utils/completion/zsh"
"github.com/urfave/cli"
)

Expand Down Expand Up @@ -50,14 +52,24 @@ func (s *Action) CompletionBash(c *cli.Context) error {
return nil
}

// CompletionZSH returns a script that uses bash's auto completion
func (s *Action) CompletionZSH(c *cli.Context) error {
out := `autoload -U compinit && compinit
autoload -U bashcompinit && bashcompinit
// CompletionFish returns an autocompletion script for fish
func (s *Action) CompletionFish(c *cli.Context, a *cli.App) error {
comp, err := fishcomp.GetCompletion(a)
if err != nil {
return err
}

`
out += "source <(" + s.Name + " completion bash)"
fmt.Println(out)
fmt.Println(comp)
return nil
}

// CompletionZSH returns a zsh completion script
func (s *Action) CompletionZSH(c *cli.Context, a *cli.App) error {
comp, err := zshcomp.GetCompletion(a)
if err != nil {
return err
}

fmt.Println(comp)
return nil
}
16 changes: 15 additions & 1 deletion action/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"os"
"strings"
"testing"

"github.com/urfave/cli"
)

func TestBashEscape(t *testing.T) {
Expand All @@ -30,6 +32,8 @@ func TestComplete(t *testing.T) {
t.Fatalf("Error: %s", err)
}

app := cli.NewApp()

out := capture(t, func() error {
act.Complete(nil)
return nil
Expand All @@ -38,15 +42,25 @@ func TestComplete(t *testing.T) {
t.Errorf("should return 'foo' not '%s'", out)
}

// bash
out = capture(t, func() error {
return act.CompletionBash(nil)
})
if !strings.Contains(out, "action.test") {
t.Errorf("should contain name of test")
}

// fish
out = capture(t, func() error {
return act.CompletionFish(nil, app)
})
if !strings.Contains(out, "action.test") {
t.Errorf("should contain name of test")
}

// zsh
out = capture(t, func() error {
return act.CompletionZSH(nil)
return act.CompletionZSH(nil, app)
})
if !strings.Contains(out, "action.test") {
t.Errorf("should contain name of test")
Expand Down
92 changes: 0 additions & 92 deletions fish.completion

This file was deleted.

14 changes: 11 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,17 @@ func main() {
Usage: "Source for auto completion in bash",
Action: action.CompletionBash,
}, {
Name: "zsh",
Usage: "Source for auto completion in zsh",
Action: action.CompletionZSH,
Name: "zsh",
Usage: "Source for auto completion in zsh",
Action: func(c *cli.Context) error {
return action.CompletionZSH(c, app)
},
}, {
Name: "fish",
Usage: "Source for auto completion in fish",
Action: func(c *cli.Context) error {
return action.CompletionFish(c, app)
},
}},
},
{
Expand Down
11 changes: 5 additions & 6 deletions tests/completion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ complete -F _gopass_bash_autocomplete gopass`
assert.NoError(t, err)
assert.Equal(t, bash, out)

zsh := `autoload -U compinit && compinit
autoload -U bashcompinit && bashcompinit
source <(gopass completion bash)`

out, err = ts.run("completion zsh")
assert.NoError(t, err)
assert.Equal(t, zsh, out)
assert.Contains(t, out, "compdef gopass")

out, err = ts.run("completion fish")
assert.NoError(t, err)
assert.Contains(t, out, "complete")
}
87 changes: 87 additions & 0 deletions utils/completion/fish/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package fish

import (
"bytes"
"fmt"
"html/template"
"strings"

"github.com/urfave/cli"
)

func longName(name string) string {
parts := strings.Split(name, ",")
if len(parts) < 1 {
return ""
}
return strings.TrimSpace(parts[0])
}

func shortName(name string) string {
parts := strings.Split(name, ",")
if len(parts) < 2 {
return ""
}
return strings.TrimSpace(parts[1])
}

func formatFlag(name, usage, typ string) string {
switch typ {
case "short":
return shortName(name)
case "long":
return longName(name)
case "usage":
return usage
default:
return ""
}
}

func formatFlagFunc(typ string) func(cli.Flag) (string, error) {
return func(f cli.Flag) (string, error) {
switch ft := f.(type) {
case cli.BoolFlag:
return formatFlag(ft.Name, ft.Usage, typ), nil
case cli.Float64Flag:
return formatFlag(ft.Name, ft.Usage, typ), nil
case cli.GenericFlag:
return formatFlag(ft.Name, ft.Usage, typ), nil
case cli.Int64Flag:
return formatFlag(ft.Name, ft.Usage, typ), nil
case cli.Int64SliceFlag:
return formatFlag(ft.Name, ft.Usage, typ), nil
case cli.IntFlag:
return formatFlag(ft.Name, ft.Usage, typ), nil
case cli.IntSliceFlag:
return formatFlag(ft.Name, ft.Usage, typ), nil
case cli.StringFlag:
return formatFlag(ft.Name, ft.Usage, typ), nil
case cli.StringSliceFlag:
return formatFlag(ft.Name, ft.Usage, typ), nil
case cli.Uint64Flag:
return formatFlag(ft.Name, ft.Usage, typ), nil
case cli.UintFlag:
return formatFlag(ft.Name, ft.Usage, typ), nil
default:
return "", fmt.Errorf("unknown type: '%T'", f)
}
}
}

func GetCompletion(a *cli.App) (string, error) {
tplFuncs := template.FuncMap{
"formatShortFlag": formatFlagFunc("short"),
"formatLongFlag": formatFlagFunc("long"),
"formatFlagUsage": formatFlagFunc("usage"),
}
tpl, err := template.New("fish").Funcs(tplFuncs).Parse(fishTemplate)
if err != nil {
return "", err
}
buf := &bytes.Buffer{}
if err := tpl.Execute(buf, a); err != nil {
return "", err
}
return buf.String(), nil
}
35 changes: 35 additions & 0 deletions utils/completion/fish/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package fish

import (
"testing"

"github.com/urfave/cli"
)

func TestFormatFlag(t *testing.T) {
for _, tc := range []struct {
Name string
Usage string
Typ string
Out string
}{
{"print, p", "Print", "short", "p"},
{"print, p", "Print", "long", "print"},
{"print, p", "Print", "usage", "Print"},
{"print, p", "Print", "foo", ""},
} {
out := formatFlag(tc.Name, tc.Usage, tc.Typ)
if out != tc.Out {
t.Errorf("'%s' != '%s'", out, tc.Out)
}
}
}

func TestGetCompletion(t *testing.T) {
app := cli.NewApp()
sv, err := GetCompletion(app)
if err != nil {
t.Fatalf("Error: %s", err)
}
t.Logf("Output: %s", sv)
}
Loading

0 comments on commit 7f28502

Please sign in to comment.