Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add group flags #1778

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft

Add group flags #1778

wants to merge 1 commit into from

Conversation

knqyf263
Copy link

Description

Add group flags

Close #1327

Example

main.go
package main

import (
	"fmt"

	"github.com/spf13/cobra"
)

func main() {
	var output string
	var rootCmd = &cobra.Command{
		Use:   "root [sub]",
		Short: "My root command",
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Printf("Output: %s\n", output)
		},
	}
	rootCmd.Flags().Int("replicas", 0, "replicas")
	rootCmd.NamedFlags("cache").String("dir", "", "cache dir")
	rootCmd.NamedFlags("cache").Duration("max-age", 0, "cache ttl")
	rootCmd.NamedFlags("result").StringVarP(&output, "output", "o", "", "output format")
	rootCmd.NamedFlags("result").StringP("filename", "f", "", "filename")

	rootCmd.SetArgs([]string{"--output", "dummy.txt"})
	rootCmd.Execute()
	fmt.Println()
	rootCmd.SetArgs([]string{"--help"})
	rootCmd.Execute()
}
$ go run main.go
Output: dummy.txt

My root command

Usage:
  root [sub] [flags]

Flags:
  -h, --help           help for root
      --replicas int   replicas

Cache Flags:
      --dir string         cache dir
      --max-age duration   cache ttl

Result Flags:
  -f, --filename string   filename
  -o, --output string     output format

@CLAassistant
Copy link

CLAassistant commented Aug 18, 2022

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions bot added the size/L Denotes a PR that chanes 100-199 lines label Aug 18, 2022
@@ -0,0 +1,89 @@
package cobra
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this file be in spf13/pflag?

}

// NamedFlags returns the specific named FlagSet that applies to this command.
func (c *Command) NamedFlags(name string) *flag.FlagSet {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NamedFlags doesn't support persistent flags at the moment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any idea of what should be done to add this support?

@knqyf263
Copy link
Author

@marckhouzam I don't think this is the best implementation, but I hope this is a good starting point for discussion.

I borrowed an idea of NamedFlagSets from Kubernetes.
https://github.com/kubernetes/component-base/blob/b5a495af30a7bb04642ce82f4816b47e75f78dbe/cli/flag/sectioned.go#L33-L41

@github-actions
Copy link

The Cobra project currently lacks enough contributors to adequately respond to all PRs. This bot triages issues and PRs according to the following rules:

  • After 60d of inactivity, lifecycle/stale is applied. - After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied and the PR is closed.
    You can:
  • Make a comment to remove the stale label and show your support. The 60 days reset. - If a PR has lifecycle/rotten and is closed, comment and ask maintainers if they'd be interseted in reopening.

@github-actions
Copy link

The Cobra project currently lacks enough contributors to adequately respond to all PRs. This bot triages issues and PRs according to the following rules:

  • After 60d of inactivity, lifecycle/stale is applied. - After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied and the PR is closed.
    You can:
  • Make a comment to remove the stale label and show your support. The 60 days reset. - If a PR has lifecycle/rotten and is closed, comment and ask maintainers if they'd be interested in reopening.

@EraYaN
Copy link

EraYaN commented Jun 26, 2023

This would still be a very valuable feature and should not go stale.

Copy link

@eiffel-fl eiffel-fl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi.

I tested this PR and it works fine:

$ tail go.mod
go 1.18

require github.com/spf13/cobra v1.7.0

require (
        github.com/inconshreveable/mousetrap v1.1.0 // indirect
        github.com/spf13/pflag v1.0.5 // indirect
)

replace github.com/spf13/cobra => github.com/knqyf263/cobra v1.5.1-0.20220818091539-ec6fe6b61cd4
$ go run .
Output: dummy.txt

My root command

Usage:
  root [sub] [flags]

Flags:
  -h, --help           help for root
      --replicas int   replicas

Cache Flags:
      --dir string         cache dir
      --max-age duration   cache ttl

Result Flags:
  -f, --filename string   filename
  -o, --output string     output format

I also applied the given patch to latest spf13/cobra main branch and it applies cleanly.
This contribution would really help the community once merged, so I think it is totally worth it continuing to work on it.
I have some comments regarding the code but nothing critical with regard to my basic knowledge of spf13/cobra.

Best regards.

}

// NamedFlags returns the specific named FlagSet that applies to this command.
func (c *Command) NamedFlags(name string) *flag.FlagSet {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any idea of what should be done to add this support?

// NamedFlagSets returns all the named FlagSet that applies to this command.
func (c *Command) NamedFlagSets() *NamedFlagSets {
if c.lnamedFlagSets == nil {
c.lnamedFlagSets = NewNamedFlagSets(c.Name(), flag.ContinueOnError)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the flat.ContinueOnError be hardcoded?

Comment on lines +32 to +34
if nfs.flagSets == nil {
nfs.flagSets = map[string]*pflag.FlagSet{}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this operation be done in NewNamedFlagSets()?

}

func (nfs *NamedFlagSets) FlagUsagesWrapped(cols int) string {
var buf bytes.Buffer

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to use a string builder?

Comment on lines +79 to +83
if cols > 24 {
i := strings.Index(s, zzz)
lines := strings.Split(s[:i], "\n")
fmt.Fprint(&buf, strings.Join(lines[:len(lines)-1], "\n"))
fmt.Fprintln(&buf)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this code really needed? And why 24?

@pedromotita
Copy link
Contributor

Hi, is there any updates do this PR status? It's a really nice feature. A lot of projects would benefit from it

@knqyf263
Copy link
Author

@pedromotita I wanted to kick off the discussion, but I've not received any response from maintainers.

@eiffel-fl
Copy link

Hi.

@pedromotita I wanted to kick off the discussion, but I've not received any response from maintainers.

I am not in maintainers heads, but you can try to publish this PR as ready for review and they may take a look (cost nothing to try by the way).

Best regards.

@marckhouzam
Copy link
Collaborator

Thanks @knqyf263 for your patience (and everyone else interested in this), I will start looking at this PR.

@marckhouzam
Copy link
Collaborator

There is a PR to support this in spf13/pflag directly: spf13/pflag#339
I'm mentioning this as it can be a reference, however, there has been no movement in that project for years, so I think we should continue adding support in Cobra instead.

@marckhouzam
Copy link
Collaborator

@knqyf263 Although I think your API is very elegant, I'm concerned it complicates the code in Cobra and would make maintenance more difficult. I'm worried about creating yet another "type" of flags and then have: local flags, inherited flags and named flags.

The grouping of flags in the help does not affect their actual behaviour. So from a behavioural perspective we still have either local or inherited flags. I would prefer keeping those two groups and adding an independent concept of "flag group" on top of this.

For better or worse we already have an API for command groups (see doc here), so it may be good for users of Cobra to have a similar API for flag groups.

With that in mind, what would you think of an API that looked something like:

var rootCmd = &cobra.Command{
	Use:   "root [sub]",
	Short: "My root command",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("Root command")
	},
}

// Create flags exactly like is currently done
rootCmd.Flags().Int("replicas", 0, "replicas")
rootCmd.Flags.()String("dir", "", "cache dir")
rootCmd.Flags().Duration("max-age", 0, "cache ttl")
rootCmd.PersistentFlags().StringVarP(&output, "output", "o", "", "output format")
rootCmd.PersistentFlags().StringP("filename", "f", "", "filename")

// Add new, separate knowledge for grouping flags in the help output
err = rootCmd.AddFlagHelpGroup(cobra.Group{ID: "group1", "Title": "cache"}, cobra.Group{ID: "group2", "Title": "result"})
err = rootCmd.AddFlagToHelpGroupID("dir", "group1")
err = rootCmd.AddFlagToHelpGroupID("max-age", "group1")
err = rootCmd.AddFlagToHelpGroupID("output", "group2")
err = rootCmd.AddFlagToHelpGroupID("filename", "group2")

Notice the use of the word "Help" to qualify the flag groups; I think we need something along those lines because Cobra has a concept of flag group for mutual exclusion and such things.

I admit I like your API better, but I am hoping that something along the lines of the above will be safer to implement. And it should handle global flags transparently, I'm hoping.

Hopefully this can start a discussion leading to something we all agree is useful.

@pedromotita
Copy link
Contributor

@knqyf263 any thoughts on @marckhouzam API proposal? He has a fair point

Let us know if you're still willing to work on this PR. I'm new to the community, but would be very happy to help 😄

@pedromotita
Copy link
Contributor

@marckhouzam I've been trying to wrap my head around a solution using your API suggestion. The main problem is that flag.FlagSet has no reference to cobra.Group.

Whereas dealing with Command we can use {{if (and (eq .GroupID $group.ID) ... }} in the template, we can't do that for flags... We could create a map do tie a Group.ID to a flag.FlagSet, but it's not a elegant solution at all...

The more I think about this feature, the more I get convinced part of it should be implemented in spf13/pflag package 😢

@marckhouzam
Copy link
Collaborator

I believe flags have an annotation field. Maybe you can use that to associate the group Id?

@pedromotita
Copy link
Contributor

@marckhouzam I had not thought of that. Thanks!

I opened the PR #2117 with your API proposal and some test cases :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
size/L Denotes a PR that chanes 100-199 lines
Projects
None yet
Development

Successfully merging this pull request may close these issues.

group flags for a subcommand
6 participants