From 8e9ea05a6733f1e253aa2b4edef0859d119d49b0 Mon Sep 17 00:00:00 2001 From: Anton Bordwine <54022438+antonchuvashow@users.noreply.github.com> Date: Fri, 24 Apr 2026 20:23:12 +0500 Subject: [PATCH] listremotes: add --exact flag for filtering - fixes #9076 --- cmd/listremotes/listremotes.go | 100 ++++++++++++++++++---------- cmd/listremotes/listremotes_test.go | 53 +++++++++++++++ 2 files changed, 118 insertions(+), 35 deletions(-) create mode 100644 cmd/listremotes/listremotes_test.go diff --git a/cmd/listremotes/listremotes.go b/cmd/listremotes/listremotes.go index 0a5a98c1e..da00af38b 100644 --- a/cmd/listremotes/listremotes.go +++ b/cmd/listremotes/listremotes.go @@ -20,6 +20,7 @@ import ( var ( listLong bool jsonOutput bool + exactMatch bool filterName string filterType string filterSource string @@ -35,13 +36,66 @@ func init() { flags.StringVarP(cmdFlags, &filterType, "type", "", "", "Filter remotes by type", "") flags.StringVarP(cmdFlags, &filterSource, "source", "", "", "Filter remotes by source, e.g. 'file' or 'environment'", "") flags.StringVarP(cmdFlags, &filterDescription, "description", "", "", "Filter remotes by description", "") + flags.BoolVarP(cmdFlags, &exactMatch, "exact", "", false, "Match filter strings exactly instead of using non-anchored glob matching", "") flags.StringVarP(cmdFlags, &orderBy, "order-by", "", "", "Instructions on how to order the result, e.g. 'type,name=descending'", "") flags.BoolVarP(cmdFlags, &jsonOutput, "json", "", false, "Format output as JSON", "") } -// lessFn compares to remotes for order by +// lessFn compares two remotes for order by. type lessFn func(a, b config.Remote) bool +// compileFilters compiles all configured filters into regexps. +func compileFilters(filterAll string, exact bool) (map[string]*regexp.Regexp, error) { + filters := make(map[string]*regexp.Regexp) + for k, v := range map[string]string{ + "all": filterAll, + "name": filterName, + "type": filterType, + "source": filterSource, + "description": filterDescription, + } { + if v == "" { + continue + } + filterRe, err := filter.GlobStringToRegexp(v, exact, true) + if err != nil { + return nil, fmt.Errorf("invalid %s filter argument: %w", k, err) + } + fs.Debugf(nil, "Filter for %s: %s", k, filterRe.String()) + filters[k] = filterRe + } + return filters, nil +} + +// includeRemote returns true if remote matches all configured filters. +func includeRemote(remote config.Remote, filters map[string]*regexp.Regexp) bool { + for k, v := range filters { + switch k { + case "all": + if !(v.MatchString(remote.Name) || v.MatchString(remote.Type) || v.MatchString(remote.Source) || v.MatchString(remote.Description)) { + return false + } + case "name": + if !v.MatchString(remote.Name) { + return false + } + case "type": + if !v.MatchString(remote.Type) { + return false + } + case "source": + if !v.MatchString(remote.Source) { + return false + } + case "description": + if !v.MatchString(remote.Description) { + return false + } + } + } + return true +} + // newLess returns a function for comparing remotes based on an order by string func newLess(orderBy string) (less lessFn, err error) { if orderBy == "" { @@ -125,53 +179,29 @@ the source (file or environment). Result can be filtered by a filter argument which applies to all attributes, and/or filter flags specific for each attribute. The values must be specified -according to regular rclone filtering pattern syntax.`, +according to regular rclone filtering pattern syntax. + +By default filtering uses non-anchored matching, so ` + "`--type box`" + ` also +matches ` + "`dropbox`" + `. Use ` + "`--exact`" + ` to match complete values only.`, Annotations: map[string]string{ "versionIntroduced": "v1.34", }, RunE: func(command *cobra.Command, args []string) error { cmd.CheckArgs(0, 1, command, args) - var filterDefault string + var filterAll string if len(args) > 0 { - filterDefault = args[0] + filterAll = args[0] } - filters := make(map[string]*regexp.Regexp) - for k, v := range map[string]string{ - "all": filterDefault, - "name": filterName, - "type": filterType, - "source": filterSource, - "description": filterDescription, - } { - if v != "" { - filterRe, err := filter.GlobStringToRegexp(v, false, true) - if err != nil { - return fmt.Errorf("invalid %s filter argument: %w", k, err) - } - fs.Debugf(nil, "Filter for %s: %s", k, filterRe.String()) - filters[k] = filterRe - } + filters, err := compileFilters(filterAll, exactMatch) + if err != nil { + return err } remotes := config.GetRemotes() maxName := 0 maxType := 0 i := 0 for _, remote := range remotes { - include := true - for k, v := range filters { - if k == "all" && !(v.MatchString(remote.Name) || v.MatchString(remote.Type) || v.MatchString(remote.Source) || v.MatchString(remote.Description)) { - include = false - } else if k == "name" && !v.MatchString(remote.Name) { - include = false - } else if k == "type" && !v.MatchString(remote.Type) { - include = false - } else if k == "source" && !v.MatchString(remote.Source) { - include = false - } else if k == "description" && !v.MatchString(remote.Description) { - include = false - } - } - if include { + if includeRemote(remote, filters) { if len(remote.Name) > maxName { maxName = len(remote.Name) } diff --git a/cmd/listremotes/listremotes_test.go b/cmd/listremotes/listremotes_test.go new file mode 100644 index 000000000..8d42e1abc --- /dev/null +++ b/cmd/listremotes/listremotes_test.go @@ -0,0 +1,53 @@ +package ls + +import ( + "testing" + + "github.com/rclone/rclone/fs/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func resetFilterFlags() { + filterName = "" + filterType = "" + filterSource = "" + filterDescription = "" +} + +func TestTypeFilterDefaultIsFuzzy(t *testing.T) { + resetFilterFlags() + filterType = "box" + t.Cleanup(resetFilterFlags) + + filters, err := compileFilters("", false) + require.NoError(t, err) + + assert.True(t, includeRemote(config.Remote{Type: "box"}, filters)) + assert.True(t, includeRemote(config.Remote{Type: "dropbox"}, filters)) +} + +func TestTypeFilterExactMatchesWholeValue(t *testing.T) { + resetFilterFlags() + filterType = "box" + t.Cleanup(resetFilterFlags) + + filters, err := compileFilters("", true) + require.NoError(t, err) + + assert.True(t, includeRemote(config.Remote{Type: "box"}, filters)) + assert.True(t, includeRemote(config.Remote{Type: "BoX"}, filters)) + assert.False(t, includeRemote(config.Remote{Type: "dropbox"}, filters)) +} + +func TestPositionalFilterExactAlsoMatchesWholeValue(t *testing.T) { + resetFilterFlags() + t.Cleanup(resetFilterFlags) + + filters, err := compileFilters("box", true) + require.NoError(t, err) + + assert.True(t, includeRemote(config.Remote{Type: "box"}, filters)) + assert.False(t, includeRemote(config.Remote{Name: "mybox"}, filters)) + assert.False(t, includeRemote(config.Remote{Description: "my dropbox remote"}, filters)) +}