Skip to content

Commit

Permalink
CLI: Add lxc file completions and fix lxc profile copy completion…
Browse files Browse the repository at this point in the history
…s (from Incus) (#14749)

This PR improves completions for `lxc file` and fixes an issue with `lxc
profile copy` completions.

Summary of changes:
- Improves remote and instance completions by only completing names
based on the `toComplete` parameter.
- Adds `cmpFiles` for improved `lxc file push|pull` completions.
- Fixes an issue with `lxc profile copy` completions by making use of
the `cmpProfiles` function.

Cherry-picks from lxc/incus#1445,
lxc/incus#1453 and
lxc/incus#1454.
  • Loading branch information
tomponline authored Jan 9, 2025
2 parents c34e09d + 7f3886c commit a06777b
Show file tree
Hide file tree
Showing 15 changed files with 172 additions and 44 deletions.
6 changes: 3 additions & 3 deletions lxc/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (c *cmdClusterList) command() *cobra.Command {

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpRemotes(false)
return c.global.cmpRemotes(toComplete, false)
}

return nil, cobra.ShellCompDirectiveNoFileComp
Expand Down Expand Up @@ -689,7 +689,7 @@ func (c *cmdClusterEnable) command() *cobra.Command {

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpRemotes(false)
return c.global.cmpRemotes(toComplete, false)
}

return nil, cobra.ShellCompDirectiveNoFileComp
Expand Down Expand Up @@ -982,7 +982,7 @@ func (c *cmdClusterListTokens) command() *cobra.Command {

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpRemotes(false)
return c.global.cmpRemotes(toComplete, false)
}

return nil, cobra.ShellCompDirectiveNoFileComp
Expand Down
4 changes: 2 additions & 2 deletions lxc/cluster_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ lxc cluster group create g1 < config.yaml

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpRemotes(false)
return c.global.cmpRemotes(toComplete, false)
}

return nil, cobra.ShellCompDirectiveNoFileComp
Expand Down Expand Up @@ -445,7 +445,7 @@ func (c *cmdClusterGroupList) command() *cobra.Command {

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpRemotes(false)
return c.global.cmpRemotes(toComplete, false)
}

return nil, cobra.ShellCompDirectiveNoFileComp
Expand Down
131 changes: 116 additions & 15 deletions lxc/completion.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package main

import (
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"

Expand Down Expand Up @@ -74,7 +77,7 @@ func (g *cmdGlobal) cmpClusterGroups(toComplete string) ([]string, cobra.ShellCo
}

if !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(false)
remotes, directives := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
cmpDirectives |= directives
}
Expand Down Expand Up @@ -174,7 +177,7 @@ func (g *cmdGlobal) cmpClusterMembers(toComplete string) ([]string, cobra.ShellC
}

if !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(false)
remotes, directives := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
cmpDirectives |= directives
}
Expand Down Expand Up @@ -214,7 +217,7 @@ func (g *cmdGlobal) cmpImages(toComplete string) ([]string, cobra.ShellCompDirec
}

if !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(true)
remotes, directives := g.cmpRemotes(toComplete, true)
results = append(results, remotes...)
cmpDirectives |= directives
}
Expand Down Expand Up @@ -586,12 +589,16 @@ func (g *cmdGlobal) cmpInstances(toComplete string) ([]string, cobra.ShellCompDi
name = resource.remote + ":" + instance
}

if !strings.HasPrefix(name, toComplete) {
continue
}

results = append(results, name)
}
}

if !strings.Contains(toComplete, ":") {
remotes, _ := g.cmpRemotes(false)
remotes, _ := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
}

Expand Down Expand Up @@ -652,7 +659,7 @@ func (g *cmdGlobal) cmpInstancesAction(toComplete string, action string, flagFor
}

if !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(false)
remotes, directives := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
cmpDirectives |= directives
}
Expand Down Expand Up @@ -695,7 +702,7 @@ func (g *cmdGlobal) cmpInstancesAndSnapshots(toComplete string) ([]string, cobra
}

if !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(false)
remotes, directives := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
cmpDirectives |= directives
}
Expand Down Expand Up @@ -778,7 +785,7 @@ func (g *cmdGlobal) cmpNetworkACLs(toComplete string) ([]string, cobra.ShellComp
}

if !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(false)
remotes, directives := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
cmpDirectives |= directives
}
Expand Down Expand Up @@ -943,7 +950,7 @@ func (g *cmdGlobal) cmpNetworks(toComplete string) ([]string, cobra.ShellCompDir
}

if !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(false)
remotes, directives := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
cmpDirectives |= directives
}
Expand Down Expand Up @@ -1139,7 +1146,7 @@ func (g *cmdGlobal) cmpNetworkZones(toComplete string) ([]string, cobra.ShellCom
}

if !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(false)
remotes, directives := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
cmpDirectives |= directives
}
Expand Down Expand Up @@ -1240,7 +1247,7 @@ func (g *cmdGlobal) cmpProfiles(toComplete string, includeRemotes bool) ([]strin
}

if includeRemotes && !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(false)
remotes, directives := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
cmpDirectives |= directives
}
Expand Down Expand Up @@ -1303,7 +1310,7 @@ func (g *cmdGlobal) cmpProjects(toComplete string) ([]string, cobra.ShellCompDir
}

if !strings.Contains(toComplete, ":") {
remotes, directives := g.cmpRemotes(false)
remotes, directives := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
cmpDirectives |= directives
}
Expand All @@ -1313,18 +1320,26 @@ func (g *cmdGlobal) cmpProjects(toComplete string) ([]string, cobra.ShellCompDir

// cmpRemotes provides shell completion for remotes.
// It takes a boolean specifying whether to include all remotes or not and returns a list of remotes along with a shell completion directive.
func (g *cmdGlobal) cmpRemotes(includeAll bool) ([]string, cobra.ShellCompDirective) {
results := make([]string, 0, len(g.conf.Remotes))
func (g *cmdGlobal) cmpRemotes(toComplete string, includeAll bool) ([]string, cobra.ShellCompDirective) {
results := []string{}

for remoteName, rc := range g.conf.Remotes {
if remoteName == "local" && g.conf.DefaultRemote == "local" || remoteName == g.conf.DefaultRemote || (!includeAll && rc.Protocol != "lxd" && rc.Protocol != "") {
continue
}

if !strings.HasPrefix(remoteName, toComplete) {
continue
}

results = append(results, remoteName+":")
}

return results, cobra.ShellCompDirectiveNoSpace
if len(results) > 0 {
return results, cobra.ShellCompDirectiveNoSpace
}

return results, cobra.ShellCompDirectiveNoFileComp
}

// cmpRemoteNames provides shell completion for remote names.
Expand Down Expand Up @@ -1434,7 +1449,7 @@ func (g *cmdGlobal) cmpStoragePools(toComplete string, noSpace bool) ([]string,
}

if !strings.Contains(toComplete, ":") {
remotes, _ := g.cmpRemotes(false)
remotes, _ := g.cmpRemotes(toComplete, false)
results = append(results, remotes...)
}

Expand Down Expand Up @@ -1635,3 +1650,89 @@ func (g *cmdGlobal) cmpStoragePoolVolumes(poolName string, volumeTypes ...string

return customVolumeNames, cobra.ShellCompDirectiveNoFileComp
}

func isSymlinkToDir(path string, d fs.DirEntry) bool {
if d.Type()&fs.ModeSymlink == 0 {
return false
}

info, err := os.Stat(path)
if err != nil || !info.IsDir() {
return false
}

return true
}

// cmpFiles provides shell completions for instances and files based on the input.
//
// If `includeLocalFiles` is true, it includes local file completions relative to the `toComplete` path.
func (g *cmdGlobal) cmpFiles(toComplete string, includeLocalFiles bool) ([]string, cobra.ShellCompDirective) {
instances, directives := g.cmpInstances(toComplete)
// Append "/" to instances to indicate directory-like behavior.
for i := range instances {
if strings.HasSuffix(instances[i], ":") {
continue
}

instances[i] += string(filepath.Separator)
}

directives |= cobra.ShellCompDirectiveNoSpace

// Early return when no instances are found.
if len(instances) == 0 {
if includeLocalFiles {
return nil, cobra.ShellCompDirectiveDefault
}

return instances, directives
}

// Early return when not including local files.
if !includeLocalFiles {
return instances, directives
}

var files []string
sep := string(filepath.Separator)
dir, prefix := filepath.Split(toComplete)
if prefix == "." || prefix == ".." {
files = append(files, dir+prefix+sep)
}

root, err := filepath.EvalSymlinks(filepath.Dir(dir))
if err != nil {
return append(instances, files...), directives
}

_ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil || path == root {
return err
}

base := filepath.Base(path)
if strings.HasPrefix(base, prefix) {
// Match files and directories starting with the given prefix.
file := dir + base
switch {
case d.IsDir():
file += sep
case isSymlinkToDir(path, d):
if base == prefix {
file += sep
}
}

files = append(files, file)
}

if d.IsDir() {
return fs.SkipDir
}

return nil
})

return append(instances, files...), directives
}
2 changes: 1 addition & 1 deletion lxc/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The pull transfer mode is the default as it is compatible with all LXD versions.
}

if len(args) == 1 {
return c.global.cmpRemotes(false)
return c.global.cmpRemotes(toComplete, false)
}

return nil, cobra.ShellCompDirectiveNoFileComp
Expand Down
36 changes: 31 additions & 5 deletions lxc/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ lxc file create --type=symlink foo/bar baz

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
return c.global.cmpFiles(toComplete, false)
}

return nil, cobra.ShellCompDirectiveNoFileComp
Expand Down Expand Up @@ -331,6 +331,10 @@ func (c *cmdFileDelete) command() *cobra.Command {
return nil, cobra.ShellCompDirectiveNoFileComp
}

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return c.global.cmpFiles(toComplete, false)
}

return cmd
}

Expand Down Expand Up @@ -388,6 +392,14 @@ func (c *cmdFileEdit) command() *cobra.Command {
return nil, cobra.ShellCompDirectiveNoFileComp
}

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpFiles(toComplete, false)
}

return nil, cobra.ShellCompDirectiveNoFileComp
}

return cmd
}

Expand Down Expand Up @@ -461,14 +473,15 @@ func (c *cmdFilePull) command() *cobra.Command {

cmd.Flags().BoolVarP(&c.file.flagMkdir, "create-dirs", "p", false, i18n.G("Create any directories necessary"))
cmd.Flags().BoolVarP(&c.file.flagRecursive, "recursive", "r", false, i18n.G("Recursively transfer files"))

cmd.RunE = c.run

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
return c.global.cmpFiles(toComplete, false)
}

return nil, cobra.ShellCompDirectiveNoFileComp
return c.global.cmpFiles(toComplete, true)
}

return cmd
Expand Down Expand Up @@ -697,8 +710,17 @@ func (c *cmdFilePush) command() *cobra.Command {
cmd.Flags().IntVar(&c.file.flagUID, "uid", -1, i18n.G("Set the file's uid on push")+"``")
cmd.Flags().IntVar(&c.file.flagGID, "gid", -1, i18n.G("Set the file's gid on push")+"``")
cmd.Flags().StringVar(&c.file.flagMode, "mode", "", i18n.G("Set the file's perms on push")+"``")

cmd.RunE = c.run

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return nil, cobra.ShellCompDirectiveDefault
}

return c.global.cmpFiles(toComplete, true)
}

return cmd
}

Expand Down Expand Up @@ -824,7 +846,7 @@ func (c *cmdFilePush) run(cmd *cobra.Command, args []string) error {
defer reverter.Fail()

// Make sure all of the files are accessible by us before trying to push any of them
var files []*os.File
files := make([]*os.File, 0, len(sourcefilenames))
for _, f := range sourcefilenames {
var file *os.File
if f == "-" {
Expand Down Expand Up @@ -1218,7 +1240,11 @@ func (c *cmdFileMount) command() *cobra.Command {

cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
return c.global.cmpInstances(toComplete)
return c.global.cmpFiles(toComplete, false)
}

if len(args) == 1 {
return nil, cobra.ShellCompDirectiveDefault
}

return nil, cobra.ShellCompDirectiveNoFileComp
Expand Down
Loading

0 comments on commit a06777b

Please sign in to comment.