pflag icon indicating copy to clipboard operation
pflag copied to clipboard

Add capability to restrict flag values to a set of allowed values

Open ian-howell opened this issue 5 years ago • 18 comments

I have a use-case where I need to perform validation on flag values. In other words, I need to check that the value passed in by the user belongs to some set of valid values. I end up writing a lot of code that looks like this:

package main

import (
	"fmt"
	"os"

	flag "github.com/spf13/pflag"
)

var color string

func main() {
	addFlags()
	flag.Parse()
	if err := validateFlags(); err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v", err)
		os.Exit(1)
	}
	fmt.Printf("Selected color is %s\n", color)
}

func addFlags() {
	flag.StringVar(&color, "color", "", "The color for the example")
}

func validateFlags() error {
	validColors := []string{"red", "blue", "yellow"}
	for _, validColor := range validColors {
		if color == validColor {
			// color is valid
			return nil
		}
	}
	// if we're here, color is invalid
	return fmt.Errorf("Value '%s' is invalid for flag 'color'. Valid values "+
		"come from the set %v", color, validColors)
}

I'd like to be able to do something like the following:

	flag.StringVarRestricted(
		&color,                            // pointer to variable
		"color",                           // flag name
		"",                                // default value
		"The color for the example",       // description
		[]string{"red", "blue", "yellow"}) // Allowed values

I wouldn't mind taking a shot at implementing something like this if it sounds acceptable.

ian-howell avatar Jan 23 '20 15:01 ian-howell

Just dropping a comment noting that this is the most thumbs-upped issue in this repo, and I'd love to see it come to fruition 🙂

Right now I do some reflection-gymnastics based on pre-defined types to validate and define flags, so if this were added in this project it'd obviate the need for that custom logic and make my codebase that much easier to maintain. I see #237 was opened, but looks like it didn't get much traction?

macintacos avatar Aug 24 '20 21:08 macintacos

Python's argparse supports flags with predefined choices. I want to replicate the same behaviour in Cobra, and think it would be a great addition.

d3an avatar Oct 14 '20 17:10 d3an

This is would be so useful. Is there anything keeping this from getting merged? 🙏

migueleliasweb avatar Jan 27 '21 00:01 migueleliasweb

I think this feature would be awesome, I would like to suggest a change, instead a slice of valid values could be a bool function that validates the value 😃

felipe-avelar avatar Jan 28 '21 15:01 felipe-avelar

@felipe-avelar that is a better approach as it is more flexiable, so using the initial example it would become:

	flag.StringVarValidated(
		&color,                            // pointer to variable
		"color",                           // flag name
		"",                                // default value
		"The color for the example",       // description
		func (s string) bool { ... }(),    // Function to evaluate valid values

pmenglund avatar Jan 28 '21 15:01 pmenglund

Alternative POV: If you provide a slice of valid values, then help output can tell the user what they are, and shell autocomplete integration can leverage that information.

Of course, not all cases can be handled by a slice, so this is probably a case of "yes, and". For example, could have the validation function, and an (optional) slice of "suggested" values?

fastcat avatar Jan 28 '21 15:01 fastcat

@fastcat You make a great point.

@pmenglund I like your suggestion and think it should be a feature either way..

I will try to find time soon to implement this on my fork, which also has other features, and works with cobra (instructions in the readme).

cornfeedhobo avatar Jan 28 '21 18:01 cornfeedhobo

Is this project dead? How come this issue is still open and stale? Where are the repo admins? cc: @spf13

thalesfsp avatar May 18 '21 02:05 thalesfsp

@thalesfsp last I check this is an open source project, so you either avoid looking a gift horse in the mouth and accept the free software with its limitations, or you implement the missing feature you want on your own (or pay someone to do it for you).

pmenglund avatar May 18 '21 14:05 pmenglund

@thalesfsp last I check this is an open source project, so you either avoid looking a gift horse in the mouth and accept the free software with its limitations, or you implement the missing feature you want on your own (or pay someone to do it for you).

Why is this snarky reply so popular? The question is completely valid given the neglect this library sees and the outright hostility everyone encounters when touching the subject on the cobra repos.

A fork has been floated on so many issues with this repo and everyone keeps shooting it down and complaining when people mention it, so please don't act like that's a real solution for the average user. If you have any experience in the industry, you know that's a straw argument intended to silence people.

cornfeedhobo avatar May 19 '21 01:05 cornfeedhobo

It is popular because so many put demands on people who devote their free time to maintaining open source projects, with no compensation but the "fame". My guess is that @spf13 has a demanding day-job, and probably a family, so any sliver of spare time is probably spent on something to unwind.

So what are the alternatives? You can whine or you can do something to fix it. Whining gets you snarky comments from people who have maintained open source projects.

That said, I should not have posted the snarky comment. Instead I should have offered advice, but my patience quota had run out.

Now it has been refilled, so here goes:

Go makes it easy very to fork this repo and use a replace directive in your go.mod to swap it out for your own fork, where you merged #237. That is a solution the average user can use. Not optimal but workable.

Since @cornfeedhobo has forked it, @thalesfsp can just add this to go.mod and get all benefits from the fork:

replace (
    github.com/spf13/pflag => github.com/cornfeedhobo/pflag
)

pmenglund avatar May 19 '21 02:05 pmenglund

@pmenglund thanks for such a thoughtful reply. Re-reading my comment and I can see I was more incensed than I meant to be. Thanks for reading through that, and my apologies.

cornfeedhobo avatar May 19 '21 02:05 cornfeedhobo

I think this is achievable with custom flags (type), as flag is only an interface:

Sample code:

package main

import (
	"fmt"
	"strings"

	flag "github.com/spf13/pflag"
)

type enum struct {
	Allowed []string
	Value   string
}

// newEnum give a list of allowed flag parameters, where the second argument is the default
func newEnum(allowed []string, d string) *enum {
	return &enum{
		Allowed: allowed,
		Value:   d,
	}
}

func (a enum) String() string {
	return a.Value
}

func (a *enum) Set(p string) error {
	isIncluded := func(opts []string, val string) bool {
		for _, opt := range opts {
			if val == opt {
				return true
			}
		}
		return false
	}
	if !isIncluded(a.Allowed, p) {
		return fmt.Errorf("%s is not included in %s", p, strings.Join(a.Allowed, ","))
	}
	a.Value = p
	return nil
}

func (a *enum) Type() string {
	return "string"
}

// Main program starts here

func main() {
	color := newEnum([]string{"red", "green", "blue"}, "green")
	flag.VarP(color, "color", "c", "the color you want")
	flag.Parse()
	fmt.Printf("You have selected %s\n", color)
}

frankywahl avatar Sep 30 '21 19:09 frankywahl

@frankywahl is the best answer here. Trivial to extend and presumably exactly what the author intended.

gingerwizard avatar Jan 20 '22 13:01 gingerwizard

@gingerwizard Yep, so trivial that it could be added to pflag.

thalesfsp avatar Jan 21 '22 20:01 thalesfsp

While enumflag is not a generic add-on for spf13, it covers the use case of enumeration flags (and slices). Maybe that might be of some help in such cases?

thediveo avatar Feb 21 '22 20:02 thediveo

The addition of Func/FuncP in #350 (disclaimer: i'm the PR author) would also fix the issue in a more generic (and indirect) manner in the case of only one or two flags:

package main

import (
	"fmt"
	"os"
	"sort"
	"strings"

	flag "github.com/spf13/pflag"
)

func main() {
	color := ""
	flag.Func("color", "use `COLOR` for the example, or \"help\"",
		func(s string) error {
			// NOTE: keep sorted!
			validColors := []string{"blue", "red", "yellow"}

			if s == "help" {
				// special case: --color=help
				fmt.Fprintln(flag.CommandLine.Output(), "Valid colors:", strings.Join(validColors, ", "))
				os.Exit(2) // "invalid argument" error if `return flag.ErrHelp` is used :(
			}

			// sort.SearchStrings can be used since validColors is already sorted.
			i := sort.SearchStrings(validColors, s)
			if i < len(validColors) && validColors[i] == s {
				color = s
				return nil
			}
			return fmt.Errorf("invalid color %q; use --color=help to list valid colors", s)
		},
	)
	flag.Parse()
	if color == "" {
		flag.Lookup("color").Value.Set("") // stay DRY
	} else {
		fmt.Printf("Selected color is %s\n", color)
	}
}

For many such flags, however, a custom Var implementation as suggested by @frankywahl is a much better solution, though i would sort enum.Allowed before returning from newEnum to allow faster searches with sort.SearchStrings. Of course, that is merely the most straightforward implementation; there may be better alternatives to consider.

Naturally you'll have people asking if one can extend this to yet another Var implementation with a one-to-one mapping from a restricted value to a custom function, or a way to restrict a range of int values without storing all possible int values in enum.Allowed, or some other request to add more code to pflag just so their own code does not need to do it. I would not be surprised if an author/maintainer/contributor of pflag is especially wary of adding such Var implementations since pflag already has significantly more Var implementations than Go's standard flag package, even excluding the P variants:

bash$ topfuncs() { go doc $1 | grep '^func.*name.*usage' | grep -v '^func[^(]*P' ; }
bash$ wc -l <(topfuncs flag) <(topfuncs github.com/spf13/pflag)
      19 /tmp//sh-np.azCogP
      65 /tmp//sh-np.MqaD5O
      84 total

That said, i agree an implementation of Var that allows you to restrict flag values is general enough and popular enough to be a useful addition with the caveat that enum is a bad name as various proposals exist to add typed enums to Go, meaning enum might become a keyword or a built-in at some point in the future.

memreflect avatar Mar 30 '23 23:03 memreflect

Hi, an ther one solution i choosed can be to add a PersistentPreRunE qualifier to your root command to operate on all general flags, or on the command linked flags concerned by. Then you can check for the variable that has catched the value and operate a regex or a switch/case or what ever you want. This way should add a "clean" solution (i mean... readable and splitable easy in a MVC design too) to contourn the lake of time interest from the owner of this nice repo. If you need an example, please ask, i will modify my comment to show you that (but i think you got the point there).

jerome-diver avatar Oct 04 '23 13:10 jerome-diver