Skip to content

Commit

Permalink
allow sorting to be configured globally (closes #418)
Browse files Browse the repository at this point in the history
I am a heavy user of the hcloud-cli. However, I really don't like the
default sorting by server-id, and would prefer sorting by age
(see #417/#420) or name.

Currently, I always type `hcloud server list -s name` to sort by name.

since the `-s` flag is part of the `list` subcommand and not the `hcloud`
base command (like in many other software that makes use of the Cobra
framework), I cannot simply `alias hcloud-"hcloud -s name"` in my shell
to default the sorting to "by name".

This commit adds a `[defaults]` section to the config toml which allows
to override the default sorting behavior for individual `hcloud`
subcommands.

The following behavior is used to determine the sorting-order

| config file | flag    | result               |
|-------------|---------|----------------------|
| not-set     | not-set | sort by id (default) |
| not-set     | set     | sort by flag value   |
| set         | not-set | sort by config value |
| set         | set     | sort by flag value   |
  • Loading branch information
cedi committed Nov 16, 2022
1 parent 57cfec5 commit e3ffaf9
Show file tree
Hide file tree
Showing 24 changed files with 338 additions and 23 deletions.
2 changes: 2 additions & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/hetznercloud/cli/internal/cmd/certificate"
"github.com/hetznercloud/cli/internal/cmd/completion"
"github.com/hetznercloud/cli/internal/cmd/config"
"github.com/hetznercloud/cli/internal/cmd/context"
"github.com/hetznercloud/cli/internal/cmd/datacenter"
"github.com/hetznercloud/cli/internal/cmd/firewall"
Expand Down Expand Up @@ -45,6 +46,7 @@ func NewRootCommand(state *state.State, client hcapi2.Client) *cobra.Command {
version.NewCommand(state),
completion.NewCommand(state),
servertype.NewCommand(state, client),
config.NewCommand(state),
context.NewCommand(state),
datacenter.NewCommand(state, client),
location.NewCommand(state, client),
Expand Down
11 changes: 9 additions & 2 deletions internal/cmd/base/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type ListCmd struct {

// CobraCommand creates a command that can be registered with cobra.
func (lc *ListCmd) CobraCommand(
ctx context.Context, client hcapi2.Client, tokenEnsurer state.TokenEnsurer,
ctx context.Context, client hcapi2.Client, tokenEnsurer state.TokenEnsurer, defaults *state.SubcommandDefaults,
) *cobra.Command {
outputColumns := lc.OutputTable(client).Columns()

Expand All @@ -47,7 +47,14 @@ func (lc *ListCmd) CobraCommand(
if lc.AdditionalFlags != nil {
lc.AdditionalFlags(cmd)
}
cmd.Flags().StringSliceP("sort", "s", []string{"id:asc"}, "Determine the sorting of the result")

sortingDefault := []string{"id:asc"}
if defaults != nil && len(defaults.Sorting) > 0 {
sortingDefault = defaults.Sorting
}

cmd.Flags().StringSliceP("sort", "s", sortingDefault, "Determine the sorting of the result")

return cmd
}

Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/certificate/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command {
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
listCmd.CobraCommand(cli.Context, client, cli),
listCmd.CobraCommand(cli.Context, client, cli, cli.Config.SubcommandDefaults[cmd.Use]),
newCreateCommand(cli),
updateCmd.CobraCommand(cli.Context, client, cli),
labelCmds.AddCobraCommand(cli.Context, client, cli),
Expand Down
21 changes: 21 additions & 0 deletions internal/cmd/config/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package config

import (
"github.com/hetznercloud/cli/internal/cmd/config/sorting"
"github.com/hetznercloud/cli/internal/state"
"github.com/spf13/cobra"
)

func NewCommand(cli *state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "config [FLAGS]",
Short: "Manage config",
Args: cobra.NoArgs,
TraverseChildren: true,
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
sorting.NewSortCommand(cli),
)
return cmd
}
121 changes: 121 additions & 0 deletions internal/cmd/config/sorting/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package sorting

import (
"errors"
"strings"

"github.com/hetznercloud/cli/internal/cmd/output"
"github.com/hetznercloud/cli/internal/state"
"github.com/spf13/cobra"
)

var listTableOutput *output.Table

func init() {
listTableOutput = output.NewTable().
AddAllowedFields(SortPresentation{})
}

type SortPresentation struct {
Command string
Column string
Order string
}

func newListCommand(cli *state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "list [COMMAND]",
Short: "See the default sorting order for a command",
Args: cobra.MaximumNArgs(1),
TraverseChildren: true,
DisableFlagsInUseLine: true,
RunE: cli.Wrap(runList),
}

return cmd
}

func runList(cli *state.State, cmd *cobra.Command, args []string) error {
if len(args) == 1 {
return runListCommand(cli, cmd, args)
}

return runListAll(cli, cmd, args)
}

func runListAll(cli *state.State, cmd *cobra.Command, args []string) error {
outOpts := output.FlagsForCommand(cmd)

cols := []string{"command", "column"}

tw := listTableOutput
if err := tw.ValidateColumns(cols); err != nil {
return err
}

if !outOpts.IsSet("noheader") {
tw.WriteHeader(cols)
}

for command, defaults := range cli.Config.SubcommandDefaults {
if defaults != nil {
presentation := SortPresentation{
Command: command,
Column: strings.Join(defaults.Sorting, ", "),
Order: "",
}

tw.Write(cols, presentation)
}
}

tw.Flush()
return nil
}

func runListCommand(cli *state.State, cmd *cobra.Command, args []string) error {
outOpts := output.FlagsForCommand(cmd)

cols := []string{"column", "order"}

command := args[0]

tw := listTableOutput
if err := tw.ValidateColumns(cols); err != nil {
return err
}

if !outOpts.IsSet("noheader") {
tw.WriteHeader(cols)
}

defaults := cli.Config.SubcommandDefaults[command]

if defaults != nil {
for _, column := range defaults.Sorting {
order := "asc"

// handle special case where colum-name includes the : to specify the order
if strings.Contains(column, ":") {
columnWithOrdering := strings.Split(column, ":")
if len(columnWithOrdering) != 2 {
return errors.New("Column sort syntax invalid")
}

column = columnWithOrdering[0]
order = columnWithOrdering[1]
}

presentation := SortPresentation{
Command: command,
Column: column,
Order: order,
}

tw.Write(cols, presentation)
}
}

tw.Flush()
return nil
}
49 changes: 49 additions & 0 deletions internal/cmd/config/sorting/reset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package sorting

import (
"errors"
"fmt"
"strings"

"github.com/hetznercloud/cli/internal/state"
"github.com/spf13/cobra"
)

func newResetCommand(cli *state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "reset COMMAND",
Short: "Reset to the application default sorting order for a command",
Args: cobra.ExactArgs(1),
TraverseChildren: true,
DisableFlagsInUseLine: true,
RunE: cli.Wrap(runReset),
}

return cmd
}

func runReset(cli *state.State, cmd *cobra.Command, args []string) error {
command := strings.TrimSpace(args[0])
if command == "" {
return errors.New("invalid command")
}

if cli.Config.SubcommandDefaults == nil {
return nil
}

defaults := cli.Config.SubcommandDefaults[command]
if defaults != nil {
defaults.Sorting = nil
}

cli.Config.SubcommandDefaults[command] = defaults

if err := cli.WriteConfig(); err != nil {
return err
}

fmt.Printf("Reset sorting to the default sorting order for command '%s list'\n", command)

return nil
}
63 changes: 63 additions & 0 deletions internal/cmd/config/sorting/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package sorting

import (
"errors"
"fmt"
"strings"

"github.com/hetznercloud/cli/internal/state"
"github.com/spf13/cobra"
)

func newSetCommand(cli *state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "set COMMAND COLUMNS...",
Short: "Set the default sorting order for a command",
Long: "Configure how the list subcommand of each command sorts it output. Append `:asc` or `:desc` to the column name control sorting (Default: `:asc`). You can also sort by multiple columns",
Args: cobra.MinimumNArgs(2),
TraverseChildren: true,
DisableFlagsInUseLine: true,
RunE: cli.Wrap(runSet),
}

return cmd
}

func runSet(cli *state.State, cmd *cobra.Command, args []string) error {
command := strings.TrimSpace(args[0])
if command == "" {
return errors.New("invalid command")
}

if len(args[1:]) == 0 {
return errors.New("invalid columns")
}

columns := make([]string, len(args[1:]))
for index, columnName := range args[1:] {
columns[index] = strings.TrimSpace(columnName)
}

if cli.Config.SubcommandDefaults == nil {
cli.Config.SubcommandDefaults = make(map[string]*state.SubcommandDefaults)
}

defaults := cli.Config.SubcommandDefaults[command]
if defaults == nil {
defaults = &state.SubcommandDefaults{
Sorting: columns,
}
} else {
defaults.Sorting = columns
}

cli.Config.SubcommandDefaults[command] = defaults

if err := cli.WriteConfig(); err != nil {
return err
}

fmt.Printf("Sorting by columns '%s' by default for command '%s list'\n", strings.Join(columns, ", "), command)

return nil
}
23 changes: 23 additions & 0 deletions internal/cmd/config/sorting/sort.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package sorting

import (
"github.com/hetznercloud/cli/internal/state"
"github.com/spf13/cobra"
)

func NewSortCommand(cli *state.State) *cobra.Command {
cmd := &cobra.Command{
Use: "sort COMMAND",
Short: "Configure the default sorting order for a command",
Args: cobra.MinimumNArgs(2),
TraverseChildren: true,
DisableFlagsInUseLine: true,
}

cmd.AddCommand(
newSetCommand(cli),
newListCommand(cli),
)

return cmd
}
2 changes: 1 addition & 1 deletion internal/cmd/datacenter/datacenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command {
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
listCmd.CobraCommand(cli.Context, client, cli),
listCmd.CobraCommand(cli.Context, client, cli, cli.Config.SubcommandDefaults[cmd.Use]),
describeCmd.CobraCommand(cli.Context, client, cli),
)
return cmd
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/firewall/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command {
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
listCmd.CobraCommand(cli.Context, client, cli),
listCmd.CobraCommand(cli.Context, client, cli, cli.Config.SubcommandDefaults[cmd.Use]),
describeCmd.CobraCommand(cli.Context, client, cli),
newCreateCommand(cli),
updateCmd.CobraCommand(cli.Context, client, cli),
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/floatingip/floatingip.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command {
}
cmd.AddCommand(
updateCmd.CobraCommand(cli.Context, client, cli),
listCmd.CobraCommand(cli.Context, client, cli),
listCmd.CobraCommand(cli.Context, client, cli, cli.Config.SubcommandDefaults[cmd.Use]),
newCreateCommand(cli),
describeCmd.CobraCommand(cli.Context, client, cli),
newAssignCommand(cli),
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command {
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
listCmd.CobraCommand(cli.Context, client, cli),
listCmd.CobraCommand(cli.Context, client, cli, cli.Config.SubcommandDefaults[cmd.Use]),
deleteCmd.CobraCommand(cli.Context, client, cli),
describeCmd.CobraCommand(cli.Context, client, cli),
updateCmd.CobraCommand(cli.Context, client, cli),
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/iso/iso.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command {
DisableFlagsInUseLine: true,
}
cmd.AddCommand(
listCmd.CobraCommand(cli.Context, client, cli),
listCmd.CobraCommand(cli.Context, client, cli, cli.Config.SubcommandDefaults[cmd.Use]),
DescribeCmd.CobraCommand(cli.Context, client, cli),
)
return cmd
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/loadbalancer/load_balancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command {
}
cmd.AddCommand(
newCreateCommand(cli),
ListCmd.CobraCommand(cli.Context, client, cli),
ListCmd.CobraCommand(cli.Context, client, cli, cli.Config.SubcommandDefaults[cmd.Use]),
DescribeCmd.CobraCommand(cli.Context, client, cli),
deleteCmd.CobraCommand(cli.Context, client, cli),
updateCmd.CobraCommand(cli.Context, client, cli),
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/loadbalancertype/load_balancer_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command {
}
cmd.AddCommand(
describeCmd.CobraCommand(cli.Context, client, cli),
ListCmd.CobraCommand(cli.Context, client, cli),
ListCmd.CobraCommand(cli.Context, client, cli, cli.Config.SubcommandDefaults[cmd.Use]),
)
return cmd
}
Loading

0 comments on commit e3ffaf9

Please sign in to comment.