listremotes: added options for filtering, ordering and json output

This commit is contained in:
albertony 2023-11-04 15:49:15 +01:00 committed by Nick Craig-Wood
parent d6b0743cf4
commit 024ff6ed15
2 changed files with 211 additions and 32 deletions

View File

@ -2,60 +2,236 @@
package ls
import (
"encoding/json"
"fmt"
"os"
"regexp"
"sort"
"strings"
"github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config"
"github.com/rclone/rclone/fs/config/flags"
"github.com/rclone/rclone/fs/filter"
"github.com/spf13/cobra"
)
// Globals
var (
listLong bool
jsonOutput bool
filterName string
filterType string
filterSource string
filterDescription string
orderBy string
)
func init() {
cmd.Root.AddCommand(commandDefinition)
cmdFlags := commandDefinition.Flags()
flags.BoolVarP(cmdFlags, &listLong, "long", "", listLong, "Show the type and the description as well as names", "")
flags.BoolVarP(cmdFlags, &listLong, "long", "", false, "Show type, source and description in addition to name", "")
flags.StringVarP(cmdFlags, &filterName, "name", "", "", "Filter remotes by name", "")
flags.StringVarP(cmdFlags, &filterType, "type", "", "", "Filter remotes by type", "")
flags.StringVarP(cmdFlags, &filterSource, "source", "", "", "filter remotes by source", "")
flags.StringVarP(cmdFlags, &filterDescription, "description", "", "", "filter remotes by description", "")
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
type lessFn func(a, b config.Remote) bool
// newLess returns a function for comparing remotes based on an order by string
func newLess(orderBy string) (less lessFn, err error) {
if orderBy == "" {
return nil, nil
}
parts := strings.Split(strings.ToLower(orderBy), ",")
n := len(parts)
for i := n - 1; i >= 0; i-- {
fieldAndDirection := strings.SplitN(parts[i], "=", 2)
descending := false
if len(fieldAndDirection) > 1 {
switch fieldAndDirection[1] {
case "ascending", "asc":
case "descending", "desc":
descending = true
default:
return nil, fmt.Errorf("unknown --order-by direction %q", fieldAndDirection[1])
}
}
var field func(o config.Remote) string
switch fieldAndDirection[0] {
case "name":
field = func(o config.Remote) string {
return o.Name
}
case "type":
field = func(o config.Remote) string {
return o.Type
}
case "source":
field = func(o config.Remote) string {
return o.Source
}
case "description":
field = func(o config.Remote) string {
return o.Description
}
default:
return nil, fmt.Errorf("unknown --order-by field %q", fieldAndDirection[0])
}
var thisLess lessFn
if descending {
thisLess = func(a, b config.Remote) bool {
return field(a) > field(b)
}
} else {
thisLess = func(a, b config.Remote) bool {
return field(a) < field(b)
}
}
if i == n-1 {
less = thisLess
} else {
nextLess := less
less = func(a, b config.Remote) bool {
if field(a) == field(b) {
return nextLess(a, b)
}
return thisLess(a, b)
}
}
}
return less, nil
}
var commandDefinition = &cobra.Command{
Use: "listremotes",
Use: "listremotes [<filter>]",
Short: `List all the remotes in the config file and defined in environment variables.`,
Long: `
rclone listremotes lists all the available remotes from the config file.
rclone listremotes lists all the available remotes from the config file,
or the remotes matching an optional filter.
When used with the ` + "`--long`" + ` flag it lists the types and the descriptions too.
Prints the result in human-readable format by default, and as a simple list of
remote names, or if used with flag ` + "`--long`" + ` a tabular format including
all attributes of the remotes: name, type, source and description. Using flag
` + "`--json`" + ` produces machine-readable output instead, which always includes
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
according to regular rclone filtering pattern syntax.
`,
Annotations: map[string]string{
"versionIntroduced": "v1.34",
},
Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(0, 0, command, args)
remotes := config.FileSections()
sort.Strings(remotes)
maxlen := 1
maxlentype := 1
RunE: func(command *cobra.Command, args []string) error {
cmd.CheckArgs(0, 1, command, args)
var filterDefault string
if len(args) > 0 {
filterDefault = 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)
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
}
}
remotes := config.GetRemotes()
maxName := 0
maxType := 0
maxSource := 0
i := 0
for _, remote := range remotes {
if len(remote) > maxlen {
maxlen = len(remote)
}
t := config.FileGet(remote, "type")
if len(t) > maxlentype {
maxlentype = len(t)
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 len(remote.Name) > maxName {
maxName = len(remote.Name)
}
if len(remote.Type) > maxType {
maxType = len(remote.Type)
}
if len(remote.Source) > maxSource {
maxSource = len(remote.Source)
}
remotes[i] = remote
i++
}
}
remotes = remotes[:i]
less, err := newLess(orderBy)
if err != nil {
return err
}
if less != nil {
sliceLessFn := func(i, j int) bool {
return less(remotes[i], remotes[j])
}
sort.SliceStable(remotes, sliceLessFn)
}
if jsonOutput {
fmt.Println("[")
first := true
for _, remote := range remotes {
if listLong {
remoteType := config.FileGet(remote, "type")
description := config.FileGet(remote, "description")
fmt.Printf("%-*s %-*s %s\n", maxlen+1, remote+":", maxlentype+1, remoteType, description)
out, err := json.Marshal(remote)
if err != nil {
return fmt.Errorf("failed to marshal remote object: %w", err)
}
if first {
first = false
} else {
fmt.Printf("%s:\n", remote)
fmt.Print(",\n")
}
_, err = os.Stdout.Write(out)
if err != nil {
return fmt.Errorf("failed to write to output: %w", err)
}
}
if !first {
fmt.Println()
}
fmt.Println("]")
} else if listLong {
for _, remote := range remotes {
fmt.Printf("%-*s %-*s %-*s %s\n", maxName+1, remote.Name+":", maxType, remote.Type, maxSource, remote.Source, remote.Description)
}
} else {
for _, remote := range remotes {
fmt.Printf("%s:\n", remote.Name)
}
}
return nil
},
}

View File

@ -444,11 +444,12 @@ func SetValueAndSave(remote, key, value string) error {
return nil
}
// Remote defines a remote with a name, type and source
// Remote defines a remote with a name, type, source and description
type Remote struct {
Name string `json:"name"`
Type string `json:"type"`
Source string `json:"source"`
Description string `json:"description"`
}
var remoteEnvRe = regexp.MustCompile(`^RCLONE_CONFIG_(.+?)_TYPE=(.+)$`)
@ -482,10 +483,12 @@ func GetRemotes() []Remote {
if !remoteExists(section) {
typeValue, found := LoadedData().GetValue(section, "type")
if found {
description, _ := LoadedData().GetValue(section, "description")
remotes = append(remotes, Remote{
Name: section,
Type: typeValue,
Source: "file",
Description: description,
})
}
}