Skip to content

Commit

Permalink
Merge pull request #104 from AkihiroSuda/dev
Browse files Browse the repository at this point in the history
completion: refactor + test
  • Loading branch information
AkihiroSuda committed Mar 11, 2021
2 parents 546b667 + 5bf98a2 commit a4658aa
Show file tree
Hide file tree
Showing 27 changed files with 392 additions and 123 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)
}
}
73 changes: 73 additions & 0 deletions completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
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("--snapshotter", gbc).AssertOut("native\n")
base.Cmd(gbc).AssertOut("run\n")
base.Cmd(gbc, "--snapshotter", gbc).AssertOut("native\n")
base.Cmd("run", "-", gbc).AssertOut("--network\n")
base.Cmd("run", "-", gbc).AssertNoOut("--namespace\n") // --namespace is a global flag, not "run" flag
base.Cmd("run", "-", gbc).AssertNoOut("--cgroup-manager\n") // --cgroup-manager is a global flag, not "run" flag
base.Cmd("run", "-n", gbc).AssertOut("--network\n")
base.Cmd("run", "-n", gbc).AssertNoOut("--namespace\n") // --namespace is a global flag, not "run" flag
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", "-it", "--net", gbc).AssertOut("bridge\n")
base.Cmd("run", "-it", "--rm", "--net", gbc).AssertOut("bridge\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") // bridge is unremovable
base.Cmd("network", "rm", gbc).AssertNoOut("host\n") // host is unremovable
base.Cmd("run", "--cap-add", gbc).AssertOut("sys_admin\n")
base.Cmd("run", "--cap-add", gbc).AssertNoOut("CAP_SYS_ADMIN\n") // invalid form

// 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
24 changes: 17 additions & 7 deletions info.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
package main

import (
"context"
"fmt"
"strings"

"github.com/AkihiroSuda/nerdctl/pkg/defaults"
"github.com/AkihiroSuda/nerdctl/pkg/rootlessutil"
"github.com/containerd/cgroups"
pkgapparmor "github.com/containerd/containerd/pkg/apparmor"
"github.com/containerd/containerd/services/introspection"
ptypes "github.com/gogo/protobuf/types"
"github.com/urfave/cli/v2"
)
Expand Down Expand Up @@ -55,16 +57,10 @@ func infoAction(clicontext *cli.Context) error {
if err != nil {
return err
}
plugins, err := introService.Plugins(ctx, nil)
snapshotterPlugins, err := getSnapshotterNames(ctx, introService)
if err != nil {
return err
}
var snapshotterPlugins []string
for _, p := range plugins.Plugins {
if strings.HasPrefix(p.Type, "io.containerd.snapshotter.") && p.InitErr == nil {
snapshotterPlugins = append(snapshotterPlugins, p.ID)
}
}

fmt.Fprintf(w, "\n")
fmt.Fprintf(w, "Server:\n")
Expand Down Expand Up @@ -95,3 +91,17 @@ func infoAction(clicontext *cli.Context) error {
fmt.Fprintf(w, " ID: %s\n", daemonIntro.UUID)
return nil
}

func getSnapshotterNames(ctx context.Context, introService introspection.Service) ([]string, error) {
var names []string
plugins, err := introService.Plugins(ctx, nil)
if err != nil {
return nil, err
}
for _, p := range plugins.Plugins {
if strings.HasPrefix(p.Type, "io.containerd.snapshotter.") && p.InitErr == nil {
names = append(names, p.ID)
}
}
return names, nil
}
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
Loading

0 comments on commit a4658aa

Please sign in to comment.