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 MarkIfFlagPresentThenOthersRequired #2200

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

faizan-siddiqui
Copy link

This PR introduces a new flag group function, MarkIfFlagPresentThenOthersRequired, which enforces a "one-way required together" relationship among flags. This means that if a primary flag is set, then other dependent flags must also be set. This allows users to make certain flags conditionally required based on the presence of another flag, while maintaining flexibility when the primary flag is not provided.

Example

Consider a command called get-range that uses the flags --start and --end to specify a date range. By default, the command can run without either flag, using default start and end values (e.g., start=someDefaultStart and end=Now). However, you may want to enforce that if the user specifies an --end, they must also specify a --start.

cmd := &cobra.Command{
    Use: "get-range",
}

cmd.Flags().String("start", "", "Start date")
cmd.Flags().String("end", "", "End date")

// If --end is set, --start is required
cmd.MarkIfFlagPresentThenRequired("end", "start")
  • If the command is called with only --end=value, it will trigger an error because --start is missing.
  • If called without any flags or only with --start, it will run using the default values or the provided start date.

This change makes it easier to enforce dependencies between flags while allowing flexible defaults when the primary flag is not specified.

@CLAassistant
Copy link

CLAassistant commented Oct 23, 2024

CLA assistant check
All committers have signed the CLA.

@faizan-siddiqui
Copy link
Author

@marckhouzam Would love to get this in thanks! Let me know if you have any questions :)

@faizan-siddiqui
Copy link
Author

@dpetersen @markbates @EdwardBetts Would love to get this in thanks! Let me know if you have any questions :)

@danpalmer
Copy link

@marckhouzam is there anything we can do to get this moving? Alternatively we're happy to have discussions about it being out of scope, it just felt fairly obviously useful to us.

@marckhouzam
Copy link
Collaborator

I like the idea. I’ll put it on my queue to review. Thank you.

@faizan-siddiqui
Copy link
Author

Hi @ccoVeille thank you for the review, I have committed your suggested changes

flag_groups.go Outdated
Comment on lines 26 to 29
requiredAsGroupAnnotation = "cobra_annotation_required_if_others_set"
oneRequiredAnnotation = "cobra_annotation_one_required"
mutuallyExclusiveAnnotation = "cobra_annotation_mutually_exclusive"
ifPresentThenOthersRequiredAnnotation = "cobra_annotation_if_present_then_others_required"
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you rename them in a way they share a common prefix, not suffix

These constants are defined at the package level, so IDE will suggest them in any method when coding in that package

I would like to see something like this

Suggested change
requiredAsGroupAnnotation = "cobra_annotation_required_if_others_set"
oneRequiredAnnotation = "cobra_annotation_one_required"
mutuallyExclusiveAnnotation = "cobra_annotation_mutually_exclusive"
ifPresentThenOthersRequiredAnnotation = "cobra_annotation_if_present_then_others_required"
annotationGroupRequired = "cobra_annotation_required_if_others_set"
annotationRequiredOne = "cobra_annotation_one_required"
annotationMutuallyExclusive = "cobra_annotation_mutually_exclusive"
annotationGroupDependent = "cobra_annotation_if_present_then_others_required"

The names of the constants are just suggestions to give you an idea

Copy link

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

Copy link

This PR exceeds the recommended size of 200 lines. Please make sure you are NOT addressing multiple issues with one PR. Note this PR might be rejected due to its size.

@faizan-siddiqui
Copy link
Author

Thanks @ccoVeille I have addressed your comment and added an explicit test case for ensuring the annotation is appended as expected and is guarded against future refactorings, please review if you can :)

Copy link
Contributor

@ccoVeille ccoVeille left a comment

Choose a reason for hiding this comment

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

Thanks for working on the changes I suggested.

Let's @marckhouzam review everything now

@faizan-siddiqui
Copy link
Author

Thanks @ccoVeille for helping out with the review and suggested changes
@marckhouzam would love to get this in, please let me know if any issues

@faizan-siddiqui
Copy link
Author

Hi @marckhouzam , @ccoVeille , is there anything else you may need to have this merged in? Let me know if there are any questions.

Copy link
Contributor

@ccoVeille ccoVeille left a comment

Choose a reason for hiding this comment

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

Hi @faizan-siddiqui

The PR is OK for me in term of code. I just reviewed again, and I made a few minor feedbacks.

Please note, while I feel confident in reviewing the code, I don't find myself n
legitimate to say whether these changes, and so features additions, are fine in the context of cobra,it's design, philosophy, and history.

So here, you will have to wait for @marckhouzam to review your changes.

Comment on lines +80 to +85
// MarkIfFlagPresentThenOthersRequired marks the given flags so that if the first flag is set,
// all the other flags become required.
func (c *Command) MarkIfFlagPresentThenOthersRequired(flagNames ...string) {
if len(flagNames) < 2 {
panic("MarkIfFlagPresentThenRequired requires at least two flags")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨 Nitpicking

I'm unsure about this wording here 🤔

Shouldn't it be, something like this

Suggested change
// MarkIfFlagPresentThenOthersRequired marks the given flags so that if the first flag is set,
// all the other flags become required.
func (c *Command) MarkIfFlagPresentThenOthersRequired(flagNames ...string) {
if len(flagNames) < 2 {
panic("MarkIfFlagPresentThenRequired requires at least two flags")
}
// MarkIfFlagPresentThenOthersRequired marks the given flags so that if the first flag is set,
// all the other flags become required.
func (c *Command) MarkIfFlagPresentThenOthersRequired(flagNames ...string) {
if len(flagNames) < 2 {
panic("MarkIfFlagPresentThenOthersRequired requires at least two flags")
}

for _, v := range flagNames {
f := c.Flags().Lookup(v)
if f == nil {
panic(fmt.Sprintf("Failed to find flag %q and mark it as being in an if present then others required flag group", v))
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't you mention the method name as you do in the other panic?

Or maybe, both shouldn't mention it

for flagList, flagnameAndStatus := range data {
flags := strings.Split(flagList, " ")
primaryFlag := flags[0]
remainingFlags := flags[1:]
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we sure this won't panic if the flags is limited to one element?

for flagList, flagnameAndStatus := range ifPresentThenRequiredGroupStatus {
flags := strings.Split(flagList, " ")
primaryFlag := flags[0]
remainingFlags := flags[1:]
Copy link
Contributor

Choose a reason for hiding this comment

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

Same question about flags length being 1 or 0

func validateIfPresentThenRequiredFlagGroups(data map[string]map[string]bool) error {
for flagList, flagnameAndStatus := range data {
flags := strings.Split(flagList, " ")
primaryFlag := flags[0]
Copy link
Contributor

Choose a reason for hiding this comment

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

Split spec explains it can return nil, so here flags[0] could panic

Maybe it's not possible, maybe it's already checked somewhere else before reaching this line of code, but I'm always suspicious when I see such optimistic code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants