cmd: complete enum flag values from their choices - fixes #5847

This commit is contained in:
Demiao Huang authored and GitHub committed 2026-10-08 15:07:41 +01:00
1 parent aa308e3d7c
commit f01af78486
4 files changed
+131 -1

No files matched your search

+3
View File
@@ -557,6 +557,9 @@ func AddBackendFlags() {
func Main() {
setupRootCommand(Root)
AddBackendFlags()
if err := registerEnumFlagCompletions(Root); err != nil {
fs.Fatalf(nil, "Failed to register flag completions: %v", err)
}
if err := Root.Execute(); err != nil {
if strings.HasPrefix(err.Error(), "unknown command") && selfupdateEnabled {
Root.PrintErrf("You could use '%s selfupdate' to get latest features.\n\n", Root.CommandPath())
+39
View File
@@ -12,6 +12,7 @@ import (
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/fspath"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// Make a debug message while doing the completion.
@@ -170,3 +171,41 @@ func validArgs(cmd *cobra.Command, args []string, toComplete string) ([]string,
return completions, result
}
// registerEnumFlagCompletions adds value completion for scalar enums.
func registerEnumFlagCompletions(root *cobra.Command) (err error) {
// Cobra normally merges these flags during Execute, after registration.
root.PersistentFlags().AddFlagSet(pflag.CommandLine)
seen := make(map[*pflag.Flag]bool)
traverseCommands(root, func(command *cobra.Command) {
register := func(flag *pflag.Flag) {
if err != nil || seen[flag] || flag.Hidden {
return
}
seen[flag] = true
var value any = flag.Value
if option, ok := value.(*fs.Option); ok {
value = option.Default
}
choices, ok := value.(fs.Choices)
if !ok {
return
}
if _, exists := command.GetFlagCompletionFunc(flag.Name); exists {
return
}
err = command.RegisterFlagCompletionFunc(flag.Name, func(_ *cobra.Command, _ []string, prefix string) ([]string, cobra.ShellCompDirective) {
var matches []string
for _, choice := range choices.Choices() {
if strings.HasPrefix(strings.ToLower(choice), strings.ToLower(prefix)) {
matches = append(matches, choice)
}
}
return matches, cobra.ShellCompDirectiveNoFileComp
})
}
command.Flags().VisitAll(register)
command.PersistentFlags().VisitAll(register)
})
return err
}
+86
View File
@@ -0,0 +1,86 @@
package cmd
import (
"bytes"
"testing"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/vfs/vfscommon"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEnumFlagCompletion(t *testing.T) {
for _, test := range []struct {
name string
args []string
want string
}{
{"option", []string{"serve", "--cache-mode", ""}, "off\nminimal\nwrites\nfull\n:4\n"},
{"prefix", []string{"serve", "--cache-mode", "f"}, "full\n:4\n"},
{"equals", []string{"serve", "--cache-mode=f"}, "full\n:4\n"},
{"invalid", []string{"serve", "--cache-mode", "invalid"}, ":4\n"},
{"global", []string{"serve", "--log-level", "d"}, "DEBUG\n:4\n"},
{"inherited", []string{"serve", "http", "--log-level=D"}, "DEBUG\n:4\n"},
{"local", []string{"serve", "http", "--local-mode", ""}, "off\nminimal\nwrites\nfull\n:4\n"},
{"custom-inherited", []string{"serve", "http", "--custom-mode", ""}, "custom\n:4\n"},
{"custom", []string{"serve", "--custom-mode", ""}, "custom\n:4\n"},
{"string", []string{"serve", "--name", ""}, ":0\n"},
} {
t.Run(test.name, func(t *testing.T) {
root := &cobra.Command{Use: "rclone"}
serve := &cobra.Command{Use: "serve"}
http := &cobra.Command{Use: "http", Run: func(*cobra.Command, []string) {}}
local := vfscommon.CacheModeOff
http.Flags().Var(&local, "local-mode", "Local mode")
serve.AddCommand(http)
root.AddCommand(serve)
level := fs.LogLevelNotice
root.PersistentFlags().Var(&level, "log-level", "Log level")
option := &fs.Option{Default: vfscommon.CacheModeOff}
serve.PersistentFlags().Var(option, "cache-mode", "Cache mode")
custom := vfscommon.CacheModeOff
serve.PersistentFlags().Var(&custom, "custom-mode", "Custom mode")
require.NoError(t, serve.RegisterFlagCompletionFunc("custom-mode", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
return []string{"custom"}, cobra.ShellCompDirectiveNoFileComp
}))
serve.PersistentFlags().String("name", "", "Name")
require.NoError(t, registerEnumFlagCompletions(root))
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&bytes.Buffer{})
root.SetArgs(append([]string{"__complete"}, test.args...))
require.NoError(t, root.Execute())
assert.Equal(t, test.want, out.String())
})
}
}
func TestEnumGlobalFlagCompletion(t *testing.T) {
previous := pflag.CommandLine
pflag.CommandLine = pflag.NewFlagSet("global", pflag.ContinueOnError)
t.Cleanup(func() { pflag.CommandLine = previous })
level := fs.LogLevelNotice
pflag.CommandLine.Var(&level, "log-level", "Log level")
root := &cobra.Command{Use: "rclone"}
root.AddCommand(&cobra.Command{Use: "copy", Run: func(*cobra.Command, []string) {}})
require.NoError(t, registerEnumFlagCompletions(root))
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&bytes.Buffer{})
root.SetArgs([]string{"__complete", "copy", "--log-level", "d"})
require.NoError(t, root.Execute())
assert.Equal(t, "DEBUG\n:4\n", out.String())
}
func TestEnumFlagCompletionHidden(t *testing.T) {
root := &cobra.Command{Use: "rclone"}
mode := vfscommon.CacheModeOff
root.Flags().Var(&mode, "hidden-mode", "Hidden mode")
require.NoError(t, root.Flags().MarkHidden("hidden-mode"))
require.NoError(t, registerEnumFlagCompletions(root))
_, exists := root.GetFlagCompletionFunc("hidden-mode")
assert.False(t, exists)
}
+3 -1
View File
@@ -14,7 +14,9 @@ var completionDefinition = &cobra.Command{
Use: "completion [shell]",
Short: `Output completion script for a given shell.`,
Long: `Generates a shell completion script for rclone.
Run with ` + "`--help`" + ` to list the supported shells.`,
Run with ` + "`--help`" + ` to list the supported shells.
Enum-valued flags such as ` + "`--log-level`" + ` and ` + "`--vfs-cache-mode`" + `
also complete their supported values.`,
Annotations: map[string]string{
"versionIntroduced": "v1.33",
},