listremotes: add --exact flag for filtering - fixes #9076
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
listLong bool
|
listLong bool
|
||||||
jsonOutput bool
|
jsonOutput bool
|
||||||
|
exactMatch bool
|
||||||
filterName string
|
filterName string
|
||||||
filterType string
|
filterType string
|
||||||
filterSource string
|
filterSource string
|
||||||
@@ -35,13 +36,66 @@ func init() {
|
|||||||
flags.StringVarP(cmdFlags, &filterType, "type", "", "", "Filter remotes by type", "")
|
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, &filterSource, "source", "", "", "Filter remotes by source, e.g. 'file' or 'environment'", "")
|
||||||
flags.StringVarP(cmdFlags, &filterDescription, "description", "", "", "Filter remotes by description", "")
|
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.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", "")
|
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
|
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
|
// newLess returns a function for comparing remotes based on an order by string
|
||||||
func newLess(orderBy string) (less lessFn, err error) {
|
func newLess(orderBy string) (less lessFn, err error) {
|
||||||
if orderBy == "" {
|
if orderBy == "" {
|
||||||
@@ -125,53 +179,29 @@ the source (file or environment).
|
|||||||
|
|
||||||
Result can be filtered by a filter argument which applies to all attributes,
|
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
|
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{
|
Annotations: map[string]string{
|
||||||
"versionIntroduced": "v1.34",
|
"versionIntroduced": "v1.34",
|
||||||
},
|
},
|
||||||
RunE: func(command *cobra.Command, args []string) error {
|
RunE: func(command *cobra.Command, args []string) error {
|
||||||
cmd.CheckArgs(0, 1, command, args)
|
cmd.CheckArgs(0, 1, command, args)
|
||||||
var filterDefault string
|
var filterAll string
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
filterDefault = args[0]
|
filterAll = args[0]
|
||||||
}
|
}
|
||||||
filters := make(map[string]*regexp.Regexp)
|
filters, err := compileFilters(filterAll, exactMatch)
|
||||||
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid %s filter argument: %w", k, err)
|
return err
|
||||||
}
|
|
||||||
fs.Debugf(nil, "Filter for %s: %s", k, filterRe.String())
|
|
||||||
filters[k] = filterRe
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
remotes := config.GetRemotes()
|
remotes := config.GetRemotes()
|
||||||
maxName := 0
|
maxName := 0
|
||||||
maxType := 0
|
maxType := 0
|
||||||
i := 0
|
i := 0
|
||||||
for _, remote := range remotes {
|
for _, remote := range remotes {
|
||||||
include := true
|
if includeRemote(remote, filters) {
|
||||||
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 len(remote.Name) > maxName {
|
if len(remote.Name) > maxName {
|
||||||
maxName = len(remote.Name)
|
maxName = len(remote.Name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user