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 d5a4626
Show file tree
Hide file tree
Showing 26 changed files with 348 additions and 115 deletions.
3 changes: 2 additions & 1 deletion commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ 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
}
Expand Down
164 changes: 153 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,157 @@ 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())
}
}

// bashCompleteNetworkNames includes {"bridge","host","none"}
func bashCompleteNetworkNames(clicontext *cli.Context, exclude []string) {
excludeMap := make(map[string]struct{}, len(exclude))
for _, ex := range exclude {
excludeMap[ex] = struct{}{}
}

// To avoid nil 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"),
}

configLists, err := netutil.ConfigLists(e)
if err != nil {
return
}
w := clicontext.App.Writer
for _, configList := range configLists {
if _, ok := excludeMap[configList.Name]; !ok {
fmt.Fprintln(w, configList.Name)
}
}
for _, s := range []string{"host", "none"} {
if _, ok := excludeMap[s]; !ok {
fmt.Fprintln(w, s)
}
}
}

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)
}
}
69 changes: 69 additions & 0 deletions completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
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("--cgroup-manager", gbc).AssertOut("cgroupfs\n")
base.Cmd(gbc).AssertOut("run\n")
base.Cmd("run", "-", gbc).AssertOut("--network\n")
base.Cmd("run", "-", gbc).AssertNoOut("--namespace\n")
base.Cmd("run", "-", gbc).AssertNoOut("--cgroup-manager\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", "--net", gbc).AssertOut("host\n")
base.Cmd("run", "--restart", gbc).AssertOut("always\n")
base.Cmd("network", "inspect", gbc).AssertOut("bridge\n")
base.Cmd("network", "rm", gbc).AssertNoOut("bridge\n")
base.Cmd("network", "rm", gbc).AssertNoOut("host\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("--cgroup-manager", gbc).AssertOut("cgroupfs\n")
rawBase.Cmd(gbc).AssertOut("run\n")
// mind {"--namespace=nerdctl-test"} vs {"--namespace", "nerdctl-test"}
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
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
24 changes: 18 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,26 @@ 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")
if ncdefaults.IsSystemdAvailable() {
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
12 changes: 5 additions & 7 deletions network_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +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)

// For `nerdctl network inspect`, print built-in "bridge" as well
w := clicontext.App.Writer
fmt.Fprintln(w, "bridge")
// show network names, including "bridge"
exclude := []string{"host", "none"}
bashCompleteNetworkNames(clicontext, exclude)
}
Loading

0 comments on commit d5a4626

Please sign in to comment.