Skip to content

Commit

Permalink
completion: refactor + test
Browse files Browse the repository at this point in the history
Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
  • Loading branch information
AkihiroSuda committed Mar 11, 2021
1 parent 546b667 commit bf4bbaf
Show file tree
Hide file tree
Showing 27 changed files with 259 additions and 109 deletions.
4 changes: 3 additions & 1 deletion commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,12 @@ func newCommitOpts(clicontext *cli.Context) (*commit.Opts, error) {
}

func commitBashComplete(clicontext *cli.Context) {
if _, ok := isFlagCompletionContext(); ok {
coco := parseCompletionContext(clicontext)
if coco.boring || coco.flagTakesValue {
defaultBashComplete(clicontext)
return
}

// show container names
bashCompleteContainerNames(clicontext)
}
143 changes: 132 additions & 11 deletions completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"os"
"strings"

"github.com/AkihiroSuda/nerdctl/pkg/labels"
"github.com/AkihiroSuda/nerdctl/pkg/netutil"
"github.com/urfave/cli/v2"
)

Expand All @@ -45,16 +47,19 @@ func completionBashAction(clicontext *cli.Context) error {
# Autocompletion enabler for nerdctl.
# Usage: add 'source <(nerdctl completion bash)' to ~/.bash_profile
# _nerdctl_bash_autocomplete is from https://github.com/urfave/cli/blob/v2.3.0/autocomplete/bash_autocomplete (MIT License)
# _nerdctl_bash_autocomplete is forked from https://github.com/urfave/cli/blob/v2.3.0/autocomplete/bash_autocomplete (MIT License)
_nerdctl_bash_autocomplete() {
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
local args="${COMP_WORDS[@]:0:$COMP_CWORD}"
# make {"nerdctl", "--namespace", "=", "foo"} into {"nerdctl", "--namespace=foo"}
args="$(echo $args | sed -e 's/ = /=/g')"
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
opts=$( ${args} ${cur} --generate-bash-completion )
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
opts=$( ${args} --generate-bash-completion )
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
Expand All @@ -67,20 +72,136 @@ complete -o bashdefault -o default -o nospace -F _nerdctl_bash_autocomplete nerd
return err
}

func isFlagCompletionContext() (string, bool) {
args := os.Args
// args[len(args)-1] == "--generate-bash-completion"
// args[len(args)-2] == the current key stroke, e.g. "--ne" for "--net"
type completionContext struct {
flagName string
flagTakesValue bool
boring bool // should call the default completer
}

func parseCompletionContext(clicontext *cli.Context) (coco completionContext) {
args := os.Args // not clicontext.Args().Slice()
// args[len(args)-2] == the current key stroke, e.g. "--net"
if len(args) <= 2 {
return "", false
coco.boring = true
return
}
userTyping := args[len(args)-2]
if strings.HasPrefix(userTyping, "-") {
return userTyping, true
flagNameCandidate := strings.TrimLeft(userTyping, "-")
if !strings.HasPrefix(userTyping, "--") {
// when userTyping is like "-it", we take "-t"
flagNameCandidate = string(userTyping[len(userTyping)-1])
}
isFlagName, flagTakesValue := checkFlagName(clicontext, flagNameCandidate)
if !isFlagName {
coco.boring = true
return
}
coco.flagName = flagNameCandidate
coco.flagTakesValue = flagTakesValue
}
return "", false
return
}

// checkFlagName returns (isFlagName, flagTakesValue)
func checkFlagName(clicontext *cli.Context, flagName string) (bool, bool) {
visibleFlags := clicontext.App.VisibleFlags()
if clicontext.Command != nil && clicontext.Command.Name != "" {
visibleFlags = clicontext.Command.VisibleFlags()
}
for _, visi := range visibleFlags {
for _, visiName := range visi.Names() {
if visiName == flagName {
type valueTaker interface {
TakesValue() bool
}
vt, ok := visi.(valueTaker)
if !ok {
return true, false
}
return true, vt.TakesValue()
}
}
}
return false, false
}

func defaultBashComplete(clicontext *cli.Context) {
cli.DefaultCompleteWithFlags(clicontext.Command)(clicontext)
if clicontext.Command == nil {
cli.DefaultCompleteWithFlags(nil)(clicontext)
}

// Dirty hack to hide global app flags such as "--namespace" , "--cgroup-manager"
dummyApp := cli.NewApp()
dummyApp.Writer = clicontext.App.Writer
dummyCliContext := cli.NewContext(dummyApp, nil, nil)
cli.DefaultCompleteWithFlags(clicontext.Command)(dummyCliContext)
}

func bashCompleteImageNames(clicontext *cli.Context) {
w := clicontext.App.Writer
client, ctx, cancel, err := newClient(clicontext)
if err != nil {
return
}
defer cancel()

imageList, err := client.ImageService().List(ctx, "")
if err != nil {
return
}
for _, img := range imageList {
fmt.Fprintln(w, img.Name)
}
}

func bashCompleteContainerNames(clicontext *cli.Context) {
w := clicontext.App.Writer
client, ctx, cancel, err := newClient(clicontext)
if err != nil {
return
}
defer cancel()
containers, err := client.Containers(ctx)
if err != nil {
return
}
for _, c := range containers {
lab, err := c.Labels(ctx)
if err != nil {
continue
}
name := lab[labels.Name]
if name != "" {
fmt.Fprintln(w, name)
continue
}
fmt.Fprintln(w, c.ID())
}
}

func bashCompleteNetworkNames(clicontext *cli.Context) {
w := clicontext.App.Writer
e := &netutil.CNIEnv{
Path: clicontext.String("cni-path"),
NetconfPath: clicontext.String("cni-netconfpath"),
}
ll, err := netutil.ConfigLists(e)
if err != nil {
return
}
for _, l := range ll {
fmt.Fprintln(w, l.Name)
}
}

func bashCompleteVolumeNames(clicontext *cli.Context) {
w := clicontext.App.Writer
vols, err := getVolumes(clicontext)
if err != nil {
return
}
for _, v := range vols {
fmt.Fprintln(w, v.Name)
}
}
48 changes: 48 additions & 0 deletions completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
Copyright (C) nerdctl authors.
Copyright (C) containerd authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"testing"

"github.com/AkihiroSuda/nerdctl/pkg/testutil"
)

func TestCompletion(t *testing.T) {
testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
const gbc = "--generate-bash-completion"
base.Cmd(gbc).AssertOut("run\n")
base.Cmd("run", "-", gbc).AssertOut("--network\n")
base.Cmd("run", "-", gbc).AssertNoOut("--namespace\n")
base.Cmd("run", "-n", gbc).AssertOut("--network\n")
base.Cmd("run", "-n", gbc).AssertNoOut("--namespace\n")
base.Cmd("run", "--ne", gbc).AssertOut("--network\n")
base.Cmd("network", "inspect", gbc).AssertOut("bridge\n")

base.Cmd("pull", testutil.AlpineImage).AssertOK()
base.Cmd("run", "-i", gbc).AssertOut(testutil.AlpineImage)
base.Cmd("run", "-it", gbc).AssertOut(testutil.AlpineImage)
base.Cmd("run", "-it", "--rm", gbc).AssertOut(testutil.AlpineImage)

baseRaw := testutil.NewBase(t)
baseRaw.Args = nil // unset `--namespace="nerdctl-test"`
baseRaw.Cmd(gbc).AssertOut("run\n")
baseRaw.Cmd("--namespace", testutil.Namespace, gbc).AssertOut("run\n")
baseRaw.Cmd("--namespace", testutil.Namespace, "run", "-i", gbc).AssertOut(testutil.AlpineImage)
}
3 changes: 2 additions & 1 deletion container_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ func (x *containerInspector) Handler(ctx context.Context, found containerwalker.
}

func containerInspectBashComplete(clicontext *cli.Context) {
if _, ok := isFlagCompletionContext(); ok {
coco := parseCompletionContext(clicontext)
if coco.boring || coco.flagTakesValue {
defaultBashComplete(clicontext)
return
}
Expand Down
3 changes: 2 additions & 1 deletion exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,8 @@ func generateExecProcessSpec(ctx context.Context, clicontext *cli.Context, conta
}

func execBashComplete(clicontext *cli.Context) {
if _, ok := isFlagCompletionContext(); ok {
coco := parseCompletionContext(clicontext)
if coco.boring || coco.flagTakesValue {
defaultBashComplete(clicontext)
return
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ require (
github.com/spf13/cobra v1.1.1 // indirect
github.com/spf13/viper v1.7.1 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/urfave/cli/v2 v2.3.0
github.com/urfave/cli/v2 v2.3.1-0.20210226231114-13ded1e7c417
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -711,8 +711,9 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/urfave/cli/v2 v2.3.1-0.20210226231114-13ded1e7c417 h1:CMxKba33r2MGOvl5Zj4SmoAjhbMJ4SdSh2Ry8b0WpA4=
github.com/urfave/cli/v2 v2.3.1-0.20210226231114-13ded1e7c417/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
Expand Down
3 changes: 2 additions & 1 deletion image_convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,8 @@ func readPathsFromRecordFile(filename string) ([]string, error) {
}

func imageConvertBashComplete(clicontext *cli.Context) {
if _, ok := isFlagCompletionContext(); ok {
coco := parseCompletionContext(clicontext)
if coco.boring || coco.flagTakesValue {
defaultBashComplete(clicontext)
return
}
Expand Down
3 changes: 2 additions & 1 deletion kill.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ func killContainer(ctx context.Context, container containerd.Container, signal s
}

func killBashComplete(clicontext *cli.Context) {
if _, ok := isFlagCompletionContext(); ok {
coco := parseCompletionContext(clicontext)
if coco.boring || coco.flagTakesValue {
defaultBashComplete(clicontext)
return
}
Expand Down
3 changes: 2 additions & 1 deletion logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ func logsAction(clicontext *cli.Context) error {
}

func logsBashComplete(clicontext *cli.Context) {
if _, ok := isFlagCompletionContext(); ok {
coco := parseCompletionContext(clicontext)
if coco.boring || coco.flagTakesValue {
defaultBashComplete(clicontext)
return
}
Expand Down
16 changes: 9 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,16 @@ const (
)

func appBashComplete(clicontext *cli.Context) {
if current, ok := isFlagCompletionContext(); ok {
switch current {
case "-n", "--namespace":
bashCompleteNamespaceNames(clicontext)
return
}
coco := parseCompletionContext(clicontext)
switch coco.flagName {
case "n", "namespace":
bashCompleteNamespaceNames(clicontext)
return
}
cli.DefaultAppComplete(clicontext)
for _, subcomm := range clicontext.App.Commands {
fmt.Fprintln(clicontext.App.Writer, subcomm.Name)
}
defaultBashComplete(clicontext)
}

func bashCompleteNamespaceNames(clicontext *cli.Context) {
Expand Down
4 changes: 3 additions & 1 deletion network_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,12 @@ func networkInspectAction(clicontext *cli.Context) error {
}

func networkInspectBashComplete(clicontext *cli.Context) {
if _, ok := isFlagCompletionContext(); ok {
coco := parseCompletionContext(clicontext)
if coco.boring || coco.flagTakesValue {
defaultBashComplete(clicontext)
return
}

// show network names
bashCompleteNetworkNames(clicontext)

Expand Down
18 changes: 2 additions & 16 deletions network_rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,11 @@ func networkRmAction(clicontext *cli.Context) error {
}

func networkRmBashComplete(clicontext *cli.Context) {
if _, ok := isFlagCompletionContext(); ok {
coco := parseCompletionContext(clicontext)
if coco.boring || coco.flagTakesValue {
defaultBashComplete(clicontext)
return
}
// show network names
bashCompleteNetworkNames(clicontext)
}

func bashCompleteNetworkNames(clicontext *cli.Context) {
w := clicontext.App.Writer
e := &netutil.CNIEnv{
Path: clicontext.String("cni-path"),
NetconfPath: clicontext.String("cni-netconfpath"),
}
ll, err := netutil.ConfigLists(e)
if err != nil {
return
}
for _, l := range ll {
fmt.Fprintln(w, l.Name)
}
}
3 changes: 2 additions & 1 deletion pause.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ func pauseContainer(ctx context.Context, client *containerd.Client, id string) e
}

func pauseBashComplete(clicontext *cli.Context) {
if _, ok := isFlagCompletionContext(); ok {
coco := parseCompletionContext(clicontext)
if coco.boring || coco.flagTakesValue {
defaultBashComplete(clicontext)
return
}
Expand Down
Loading

0 comments on commit bf4bbaf

Please sign in to comment.