diff --git a/cmd/listremotes/listremotes.go b/cmd/listremotes/listremotes.go index 68dd291c6..642306ab8 100644 --- a/cmd/listremotes/listremotes.go +++ b/cmd/listremotes/listremotes.go @@ -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 + 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 []", 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 - for _, remote := range remotes { - if len(remote) > maxlen { - maxlen = len(remote) - } - t := config.FileGet(remote, "type") - if len(t) > maxlentype { - maxlentype = len(t) + 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 listLong { - remoteType := config.FileGet(remote, "type") - description := config.FileGet(remote, "description") - fmt.Printf("%-*s %-*s %s\n", maxlen+1, remote+":", maxlentype+1, remoteType, description) - } else { - fmt.Printf("%s:\n", remote) + 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 { + out, err := json.Marshal(remote) + if err != nil { + return fmt.Errorf("failed to marshal remote object: %w", err) + } + if first { + first = false + } else { + 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 }, } diff --git a/fs/config/config.go b/fs/config/config.go index 3180f6d3b..be0a2a914 100644 --- a/fs/config/config.go +++ b/fs/config/config.go @@ -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"` + 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", + Name: section, + Type: typeValue, + Source: "file", + Description: description, }) } }