diff --git a/cmd/cmd.go b/cmd/cmd.go index 06a2d53fe..77b233023 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -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()) diff --git a/cmd/completion.go b/cmd/completion.go index 59295fa3b..8e2bec71e 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -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 +} diff --git a/cmd/completion_test.go b/cmd/completion_test.go new file mode 100644 index 000000000..03e4d6b33 --- /dev/null +++ b/cmd/completion_test.go @@ -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) +} diff --git a/cmd/genautocomplete/genautocomplete.go b/cmd/genautocomplete/genautocomplete.go index 1b358dac8..6ae54328a 100644 --- a/cmd/genautocomplete/genautocomplete.go +++ b/cmd/genautocomplete/genautocomplete.go @@ -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", },