Best Practices for Handling Many Optional Flags

I’m building a Go CLI application using Cobra and Viper libraries with numerous optional flags (10+). My primary goal is to execute actions only when their corresponding flags are specifically provided by the user.

I’ve tried follwoing approach: Utilizing cmd.Flags().StringVar(flagVar, flagName, defaultValue, usage) and cmd.Flag(“flagName”).Changed

Concerns:

  • Repetition of flag names.
  • Implicit binding between flagName and flagVariable.

I’ve explored an option to use viper.BindPFlag and viper.IsSet, but it hasn’t given an answer to any of my concerns, and it feels like overkill for this relatively simple task.

Mitigations Considered:

  • Constants/Enum for Flag Names: Lessens typos, but mismatches could still occur.
  • Dedicated Flag Management Class: Potentially over-engineered for this use case.

Question:

Are there established Go patterns for managing a substantial number of optional flags that:

  • Minimize the need to repeat flag names (reducing error potential)?
  • Promote maintainability without introducing unnecessary complexity?

Looking for some lib to address these issues (or porper use of cobra/viper that I’m missing).

I unfortunately don’t have a lot of experience in this space (I use Go primarily for web APIs and CLI tools without too many optional flags usually). My only thought here is: maybe you could see how a complex project like the GitHub CLI is managing flags. Check this out for example:

And the nested commands live in pkg/cmd. You could go spelunking through that code to get ideas. You could also take a look at how Docker is dealing with a large amount of flags.

Other thoughts: you could potentially leverage struct tags. There are quite a few projects out there that do this for you but it’s not hard to roll your own. Take a look at this project for inspiration (or just add that as a dependency!).

Certainly! For managing a large number of optional flags in your Go CLI application:

Define constants for flag names to reduce typos and ensure consistency.
Group related flags under a single struct for easier management.
Consider using subcommands instead of flags for multiple actions.
Encapsulate flag logic in functions or methods for modularization.
Use flag presets for commonly used flag combinations.
As for libraries, Cobra and Viper are solid choices for building CLI applications in Go. They offer robust features for flag management and configuration parsing.

Maybe my use case is a bit strange, but what I wanted is one place connection between flag name, value, and IsSet validation. In the end, havent found anything resembling this functionality, so had to write one by myself. Pretty disappointed by amount of boilerplate needed / code duplication. If someone can recoomend any improvements, will really appreciate:

type baseFlag struct {
	cmd *cobra.Command
	name string
}

type StrFlag struct {
	baseFlag
	Value *string
}

type Int64Flag struct {
	baseFlag
	Value *int64
}

type BoolFlag struct {
	baseFlag
	Value *bool
}

func (f *baseFlag)IsChanged() bool {
	return f.cmd.Flags().Changed(f.name)
}

func NewStr(cmd *cobra.Command, name string, defaultValue string, usage string) (*StrFlag) {
	var value *string = new(string)
	cmd.Flags().StringVar(value, name, defaultValue, usage)
	return &StrFlag{ baseFlag: baseFlag{cmd, name}, Value: value}
}

func NewStrP(cmd *cobra.Command, name string, shorthand string, defaultValue string, usage string) (*StrFlag) {
	var value *string = new(string)
	cmd.Flags().StringVarP(value, name, shorthand, defaultValue, usage)
	return &StrFlag{ baseFlag: baseFlag{cmd, name}, Value: value}
}

func NewInt64(cmd *cobra.Command, name string, defaultValue int64, usage string) (*Int64Flag) {
	var value *int64 = new(int64)
	cmd.Flags().Int64Var(value, name, defaultValue, usage)
	return &Int64Flag{ baseFlag: baseFlag{cmd, name}, Value: value}
}

func NewInt64P(cmd *cobra.Command, name string, shorthand string, defaultValue int64, usage string) (*Int64Flag) {
	var value *int64 = new(int64)
	cmd.Flags().Int64VarP(value, name, shorthand, defaultValue, usage)
	return &Int64Flag{ baseFlag: baseFlag{cmd, name}, Value: value}
}

func NewBool(cmd *cobra.Command, name string, defaultValue bool, usage string) (*BoolFlag) {
	var value *bool = new(bool)
	cmd.Flags().BoolVar(value, name, defaultValue, usage)
	return &BoolFlag{ baseFlag: baseFlag{cmd, name}, Value: value}
}

func NewBoolP(cmd *cobra.Command, name string, shorthand string, defaultValue bool, usage string) (*BoolFlag) {
	var value *bool = new(bool)
	cmd.Flags().BoolVarP(value, name, shorthand, defaultValue, usage)
	return &BoolFlag{ baseFlag: baseFlag{cmd, name}, Value: value}
}

with usage as following:

maxStartTime	*managedflag.StrFlag
maxStartTime = managedflag.NewStr(listEventsCmd, "maxStartTime", "", "list events with start times earlier than")
if (maxStartTime.IsChanged()) {
	filter = filter.MaxStartTime(*maxStartTime.Value)
}