diff --git a/internal/cli/root.go b/internal/cli/root.go index 4a66b989..a68e7ea7 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -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" @@ -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), diff --git a/internal/cmd/base/list.go b/internal/cmd/base/list.go index 59990dfb..b1c2e739 100644 --- a/internal/cmd/base/list.go +++ b/internal/cmd/base/list.go @@ -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() @@ -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 } diff --git a/internal/cmd/certificate/certificate.go b/internal/cmd/certificate/certificate.go index 48afe723..3cd3aa84 100644 --- a/internal/cmd/certificate/certificate.go +++ b/internal/cmd/certificate/certificate.go @@ -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), diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go new file mode 100644 index 00000000..f1769df4 --- /dev/null +++ b/internal/cmd/config/config.go @@ -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 +} diff --git a/internal/cmd/config/sorting/list.go b/internal/cmd/config/sorting/list.go new file mode 100644 index 00000000..bc040ace --- /dev/null +++ b/internal/cmd/config/sorting/list.go @@ -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 +} diff --git a/internal/cmd/config/sorting/reset.go b/internal/cmd/config/sorting/reset.go new file mode 100644 index 00000000..fa38de52 --- /dev/null +++ b/internal/cmd/config/sorting/reset.go @@ -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 +} diff --git a/internal/cmd/config/sorting/set.go b/internal/cmd/config/sorting/set.go new file mode 100644 index 00000000..233ba0fa --- /dev/null +++ b/internal/cmd/config/sorting/set.go @@ -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 +} diff --git a/internal/cmd/config/sorting/sort.go b/internal/cmd/config/sorting/sort.go new file mode 100644 index 00000000..7a0cc112 --- /dev/null +++ b/internal/cmd/config/sorting/sort.go @@ -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 +} diff --git a/internal/cmd/datacenter/datacenter.go b/internal/cmd/datacenter/datacenter.go index 2eb1ae4e..c95beb75 100644 --- a/internal/cmd/datacenter/datacenter.go +++ b/internal/cmd/datacenter/datacenter.go @@ -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 diff --git a/internal/cmd/firewall/firewall.go b/internal/cmd/firewall/firewall.go index fdc55aab..fa71bbe7 100644 --- a/internal/cmd/firewall/firewall.go +++ b/internal/cmd/firewall/firewall.go @@ -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), diff --git a/internal/cmd/floatingip/floatingip.go b/internal/cmd/floatingip/floatingip.go index e99d1309..4d5d178b 100644 --- a/internal/cmd/floatingip/floatingip.go +++ b/internal/cmd/floatingip/floatingip.go @@ -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), diff --git a/internal/cmd/image/image.go b/internal/cmd/image/image.go index 27b1f908..aa74f88c 100644 --- a/internal/cmd/image/image.go +++ b/internal/cmd/image/image.go @@ -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), diff --git a/internal/cmd/iso/iso.go b/internal/cmd/iso/iso.go index 2d655387..4ed80ae4 100644 --- a/internal/cmd/iso/iso.go +++ b/internal/cmd/iso/iso.go @@ -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 diff --git a/internal/cmd/loadbalancer/load_balancer.go b/internal/cmd/loadbalancer/load_balancer.go index 37917e28..71a4b6c6 100644 --- a/internal/cmd/loadbalancer/load_balancer.go +++ b/internal/cmd/loadbalancer/load_balancer.go @@ -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), diff --git a/internal/cmd/loadbalancertype/load_balancer_type.go b/internal/cmd/loadbalancertype/load_balancer_type.go index 0c4e0416..5cb6faf6 100644 --- a/internal/cmd/loadbalancertype/load_balancer_type.go +++ b/internal/cmd/loadbalancertype/load_balancer_type.go @@ -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 } diff --git a/internal/cmd/location/location.go b/internal/cmd/location/location.go index 77a6b332..706a56af 100644 --- a/internal/cmd/location/location.go +++ b/internal/cmd/location/location.go @@ -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 diff --git a/internal/cmd/network/network.go b/internal/cmd/network/network.go index 355a2dff..46229ac2 100644 --- a/internal/cmd/network/network.go +++ b/internal/cmd/network/network.go @@ -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), diff --git a/internal/cmd/placementgroup/placementgroup.go b/internal/cmd/placementgroup/placementgroup.go index 6038c56e..9de086d3 100644 --- a/internal/cmd/placementgroup/placementgroup.go +++ b/internal/cmd/placementgroup/placementgroup.go @@ -16,7 +16,7 @@ func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command { } cmd.AddCommand( CreateCmd.CobraCommand(cli.Context, client, cli, cli), - ListCmd.CobraCommand(cli.Context, client, cli), + ListCmd.CobraCommand(cli.Context, client, cli, cli.Config.SubcommandDefaults[cmd.Use]), DescribeCmd.CobraCommand(cli.Context, client, cli), UpdateCmd.CobraCommand(cli.Context, client, cli), DeleteCmd.CobraCommand(cli.Context, client, cli), diff --git a/internal/cmd/primaryip/primaryip.go b/internal/cmd/primaryip/primaryip.go index 9536e046..fad5e852 100644 --- a/internal/cmd/primaryip/primaryip.go +++ b/internal/cmd/primaryip/primaryip.go @@ -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), CreateCmd.CobraCommand(cli.Context, client, cli, cli), updateCmd.CobraCommand(cli.Context, client, cli), diff --git a/internal/cmd/server/server.go b/internal/cmd/server/server.go index 8db63878..87830acb 100644 --- a/internal/cmd/server/server.go +++ b/internal/cmd/server/server.go @@ -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), CreateCmd.CobraCommand(cli.Context, client, cli, cli), deleteCmd.CobraCommand(cli.Context, client, cli), diff --git a/internal/cmd/servertype/server_type.go b/internal/cmd/servertype/server_type.go index cf5d1823..310b9297 100644 --- a/internal/cmd/servertype/server_type.go +++ b/internal/cmd/servertype/server_type.go @@ -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 diff --git a/internal/cmd/sshkey/sshkey.go b/internal/cmd/sshkey/sshkey.go index f1e05184..2964d794 100644 --- a/internal/cmd/sshkey/sshkey.go +++ b/internal/cmd/sshkey/sshkey.go @@ -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), deleteCmd.CobraCommand(cli.Context, client, cli), diff --git a/internal/cmd/volume/volume.go b/internal/cmd/volume/volume.go index f2948cce..94d1d632 100644 --- a/internal/cmd/volume/volume.go +++ b/internal/cmd/volume/volume.go @@ -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), deleteCmd.CobraCommand(cli.Context, client, cli), diff --git a/internal/state/config.go b/internal/state/config.go index db982e65..ec19c5c2 100644 --- a/internal/state/config.go +++ b/internal/state/config.go @@ -9,9 +9,10 @@ import ( var DefaultConfigPath string type Config struct { - Endpoint string - ActiveContext *ConfigContext - Contexts []*ConfigContext + Endpoint string + ActiveContext *ConfigContext + Contexts []*ConfigContext + SubcommandDefaults map[string]*SubcommandDefaults } type ConfigContext struct { @@ -19,6 +20,11 @@ type ConfigContext struct { Token string } +type SubcommandDefaults struct { + Sorting []string + DefaultColumns []string +} + func (config *Config) ContextNames() []string { if len(config.Contexts) == 0 { return nil @@ -49,8 +55,9 @@ func (config *Config) RemoveContext(context *ConfigContext) { } type RawConfig struct { - ActiveContext string `toml:"active_context,omitempty"` - Contexts []RawConfigContext `toml:"contexts"` + ActiveContext string `toml:"active_context,omitempty"` + Contexts []RawConfigContext `toml:"contexts"` + SubcommandDefaults map[string]*RAWSubcommandDefaults `toml:"defaults,omitempty"` } type RawConfigContext struct { @@ -58,6 +65,10 @@ type RawConfigContext struct { Token string `toml:"token"` } +type RAWSubcommandDefaults struct { + Sorting []string `toml:"sort,omitempty"` +} + func MarshalConfig(c *Config) ([]byte, error) { if c == nil { return []byte{}, nil @@ -73,6 +84,15 @@ func MarshalConfig(c *Config) ([]byte, error) { Token: context.Token, }) } + if len(c.SubcommandDefaults) != 0 { + raw.SubcommandDefaults = make(map[string]*RAWSubcommandDefaults) + + for command, defaults := range c.SubcommandDefaults { + raw.SubcommandDefaults[command] = &RAWSubcommandDefaults{ + Sorting: defaults.Sorting, + } + } + } return toml.Marshal(raw) } @@ -98,5 +118,14 @@ func UnmarshalConfig(config *Config, data []byte) error { return fmt.Errorf("active context %s not found", raw.ActiveContext) } } + if len(raw.SubcommandDefaults) > 0 { + config.SubcommandDefaults = make(map[string]*SubcommandDefaults) + for command, defaults := range raw.SubcommandDefaults { + config.SubcommandDefaults[command] = &SubcommandDefaults{ + Sorting: defaults.Sorting, + } + } + } + return nil }