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 87f3363
Show file tree
Hide file tree
Showing 27 changed files with 323 additions and 112 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)
}
151 changes: 140 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,144 @@ 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
}

// 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
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
// To avoid panic during clicontext.String(),
// it seems we have to use globalcontext.String()
lineage := clicontext.Lineage()
if len(lineage) < 2 {
return
}
globalContext := lineage[len(lineage)-2]
e := &netutil.CNIEnv{
Path: globalContext.String("cni-path"),
NetconfPath: globalContext.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)
}
}
62 changes: 62 additions & 0 deletions completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
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"
// cmd is executed with base.Args={"--namespace=nerdctl-test"}
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("run", "--net", gbc).AssertOut("bridge\n")
base.Cmd("run", "--restart", gbc).AssertOut("always\n")
base.Cmd("network", "inspect", gbc).AssertOut("bridge\n")
base.Cmd("run", "--cap-add", gbc).AssertOut("sys_admin\n")
base.Cmd("run", "--cap-add", gbc).AssertNoOut("CAP_SYS_ADMIN\n")

// Tests with an image
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)

// Tests with an network
testNetworkName := "nerdctl-test-completion"
defer base.Cmd("network", "rm", testNetworkName).Run()
base.Cmd("network", "create", testNetworkName).AssertOK()
base.Cmd("network", "rm", gbc).AssertOut(testNetworkName)
base.Cmd("run", "--net", gbc).AssertOut(testNetworkName)

// Tests with raw base (without Args={"--namespace=nerdctl-test"})
rawBase := testutil.NewBase(t)
rawBase.Args = nil // unset "--namespace=nerdctl-test"
rawBase.Cmd(gbc).AssertOut("run\n")
rawBase.Cmd("--namespace", testutil.Namespace, gbc).AssertOut("run\n")
rawBase.Cmd("--namespace", testutil.Namespace, "run", "-i", gbc).AssertOut(testutil.AlpineImage)
}
14 changes: 13 additions & 1 deletion container_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,19 @@ func (x *containerInspector) Handler(ctx context.Context, found containerwalker.
}

func containerInspectBashComplete(clicontext *cli.Context) {
if _, ok := isFlagCompletionContext(); ok {
coco := parseCompletionContext(clicontext)
if coco.boring {
defaultBashComplete(clicontext)
return
}
if coco.flagTakesValue {
w := clicontext.App.Writer
switch coco.flagName {
case "mode":
fmt.Fprintln(w, "dockercompat")
fmt.Fprintln(w, "native")
return
}
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
22 changes: 16 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,24 @@ const (
)

func appBashComplete(clicontext *cli.Context) {
if current, ok := isFlagCompletionContext(); ok {
switch current {
case "-n", "--namespace":
bashCompleteNamespaceNames(clicontext)
return
w := clicontext.App.Writer
coco := parseCompletionContext(clicontext)
switch coco.flagName {
case "n", "namespace":
bashCompleteNamespaceNames(clicontext)
return
case "cgroup-manager":
fmt.Fprintln(w, "cgroupfs")
fmt.Fprintln(w, "systemd")
if rootlessutil.IsRootless() {
fmt.Fprintln(w, "none")
}
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
7 changes: 2 additions & 5 deletions network_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,11 @@ 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)

// For `nerdctl network inspect`, print built-in "bridge" as well
w := clicontext.App.Writer
fmt.Fprintln(w, "bridge")
}
Loading

0 comments on commit 87f3363

Please sign in to comment.