github.com/maresnic/mr-kong@v1.0.0/options.go (about)

     1  package kong
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/user"
     9  	"path/filepath"
    10  	"reflect"
    11  	"regexp"
    12  	"strings"
    13  )
    14  
    15  // An Option applies optional changes to the Kong application.
    16  type Option interface {
    17  	Apply(k *Kong) error
    18  }
    19  
    20  // OptionFunc is function that adheres to the Option interface.
    21  type OptionFunc func(k *Kong) error
    22  
    23  func (o OptionFunc) Apply(k *Kong) error { return o(k) } //nolint: revive
    24  
    25  // Vars sets the variables to use for interpolation into help strings and default values.
    26  //
    27  // See README for details.
    28  type Vars map[string]string
    29  
    30  // Apply lets Vars act as an Option.
    31  func (v Vars) Apply(k *Kong) error {
    32  	for key, value := range v {
    33  		k.vars[key] = value
    34  	}
    35  	return nil
    36  }
    37  
    38  // CloneWith clones the current Vars and merges "vars" onto the clone.
    39  func (v Vars) CloneWith(vars Vars) Vars {
    40  	out := make(Vars, len(v)+len(vars))
    41  	for key, value := range v {
    42  		out[key] = value
    43  	}
    44  	for key, value := range vars {
    45  		out[key] = value
    46  	}
    47  	return out
    48  }
    49  
    50  // Exit overrides the function used to terminate. This is useful for testing or interactive use.
    51  func Exit(exit func(int)) Option {
    52  	return OptionFunc(func(k *Kong) error {
    53  		k.Exit = exit
    54  		return nil
    55  	})
    56  }
    57  
    58  type embedded struct {
    59  	strct any
    60  	tags  []string
    61  }
    62  
    63  // Embed a struct into the root of the CLI.
    64  //
    65  // "strct" must be a pointer to a structure.
    66  func Embed(strct any, tags ...string) Option {
    67  	t := reflect.TypeOf(strct)
    68  	if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct {
    69  		panic("kong: Embed() must be called with a pointer to a struct")
    70  	}
    71  	return OptionFunc(func(k *Kong) error {
    72  		k.embedded = append(k.embedded, embedded{strct, tags})
    73  		return nil
    74  	})
    75  }
    76  
    77  type dynamicCommand struct {
    78  	name  string
    79  	help  string
    80  	group string
    81  	tags  []string
    82  	cmd   interface{}
    83  }
    84  
    85  // DynamicCommand registers a dynamically constructed command with the root of the CLI.
    86  //
    87  // This is useful for command-line structures that are extensible via user-provided plugins.
    88  //
    89  // "tags" is a list of extra tag strings to parse, in the form <key>:"<value>".
    90  func DynamicCommand(name, help, group string, cmd interface{}, tags ...string) Option {
    91  	return OptionFunc(func(k *Kong) error {
    92  		k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{
    93  			name:  name,
    94  			help:  help,
    95  			group: group,
    96  			cmd:   cmd,
    97  			tags:  tags,
    98  		})
    99  		return nil
   100  	})
   101  }
   102  
   103  // NoDefaultHelp disables the default help flags.
   104  func NoDefaultHelp() Option {
   105  	return OptionFunc(func(k *Kong) error {
   106  		k.noDefaultHelp = true
   107  		return nil
   108  	})
   109  }
   110  
   111  // PostBuild provides read/write access to kong.Kong after initial construction of the model is complete but before
   112  // parsing occurs.
   113  //
   114  // This is useful for, e.g., adding short options to flags, updating help, etc.
   115  func PostBuild(fn func(*Kong) error) Option {
   116  	return OptionFunc(func(k *Kong) error {
   117  		k.postBuildOptions = append(k.postBuildOptions, OptionFunc(fn))
   118  		return nil
   119  	})
   120  }
   121  
   122  // Name overrides the application name.
   123  func Name(name string) Option {
   124  	return PostBuild(func(k *Kong) error {
   125  		k.Model.Name = name
   126  		return nil
   127  	})
   128  }
   129  
   130  // Description sets the application description.
   131  func Description(description string) Option {
   132  	return PostBuild(func(k *Kong) error {
   133  		k.Model.Help = description
   134  		return nil
   135  	})
   136  }
   137  
   138  // TypeMapper registers a mapper to a type.
   139  func TypeMapper(typ reflect.Type, mapper Mapper) Option {
   140  	return OptionFunc(func(k *Kong) error {
   141  		k.registry.RegisterType(typ, mapper)
   142  		return nil
   143  	})
   144  }
   145  
   146  // KindMapper registers a mapper to a kind.
   147  func KindMapper(kind reflect.Kind, mapper Mapper) Option {
   148  	return OptionFunc(func(k *Kong) error {
   149  		k.registry.RegisterKind(kind, mapper)
   150  		return nil
   151  	})
   152  }
   153  
   154  // ValueMapper registers a mapper to a field value.
   155  func ValueMapper(ptr interface{}, mapper Mapper) Option {
   156  	return OptionFunc(func(k *Kong) error {
   157  		k.registry.RegisterValue(ptr, mapper)
   158  		return nil
   159  	})
   160  }
   161  
   162  // NamedMapper registers a mapper to a name.
   163  func NamedMapper(name string, mapper Mapper) Option {
   164  	return OptionFunc(func(k *Kong) error {
   165  		k.registry.RegisterName(name, mapper)
   166  		return nil
   167  	})
   168  }
   169  
   170  // Writers overrides the default writers. Useful for testing or interactive use.
   171  func Writers(stdout, stderr io.Writer) Option {
   172  	return OptionFunc(func(k *Kong) error {
   173  		k.Stdout = stdout
   174  		k.Stderr = stderr
   175  		return nil
   176  	})
   177  }
   178  
   179  // Bind binds values for hooks and Run() function arguments.
   180  //
   181  // Any arguments passed will be available to the receiving hook functions, but may be omitted. Additionally, *Kong and
   182  // the current *Context will also be made available.
   183  //
   184  // There are two hook points:
   185  //
   186  //			BeforeApply(...) error
   187  //	  	AfterApply(...) error
   188  //
   189  // Called before validation/assignment, and immediately after validation/assignment, respectively.
   190  func Bind(args ...interface{}) Option {
   191  	return OptionFunc(func(k *Kong) error {
   192  		k.bindings.add(args...)
   193  		return nil
   194  	})
   195  }
   196  
   197  // BindTo allows binding of implementations to interfaces.
   198  //
   199  //	BindTo(impl, (*iface)(nil))
   200  func BindTo(impl, iface interface{}) Option {
   201  	return OptionFunc(func(k *Kong) error {
   202  		k.bindings.addTo(impl, iface)
   203  		return nil
   204  	})
   205  }
   206  
   207  // BindToProvider allows binding of provider functions.
   208  //
   209  // This is useful when the Run() function of different commands require different values that may
   210  // not all be initialisable from the main() function.
   211  func BindToProvider(provider interface{}) Option {
   212  	return OptionFunc(func(k *Kong) error {
   213  		return k.bindings.addProvider(provider)
   214  	})
   215  }
   216  
   217  // Help printer to use.
   218  func Help(help HelpPrinter) Option {
   219  	return OptionFunc(func(k *Kong) error {
   220  		k.help = help
   221  		return nil
   222  	})
   223  }
   224  
   225  // ShortHelp configures the short usage message.
   226  //
   227  // It should be used together with kong.ShortUsageOnError() to display a
   228  // custom short usage message on errors.
   229  func ShortHelp(shortHelp HelpPrinter) Option {
   230  	return OptionFunc(func(k *Kong) error {
   231  		k.shortHelp = shortHelp
   232  		return nil
   233  	})
   234  }
   235  
   236  // HelpFormatter configures how the help text is formatted.
   237  //
   238  // Deprecated: Use ValueFormatter() instead.
   239  func HelpFormatter(helpFormatter HelpValueFormatter) Option {
   240  	return OptionFunc(func(k *Kong) error {
   241  		k.helpFormatter = helpFormatter
   242  		return nil
   243  	})
   244  }
   245  
   246  // ValueFormatter configures how the help text is formatted.
   247  func ValueFormatter(helpFormatter HelpValueFormatter) Option {
   248  	return OptionFunc(func(k *Kong) error {
   249  		k.helpFormatter = helpFormatter
   250  		return nil
   251  	})
   252  }
   253  
   254  // ConfigureHelp sets the HelpOptions to use for printing help.
   255  func ConfigureHelp(options HelpOptions) Option {
   256  	return OptionFunc(func(k *Kong) error {
   257  		k.helpOptions = options
   258  		return nil
   259  	})
   260  }
   261  
   262  // AutoGroup automatically assigns groups to flags.
   263  func AutoGroup(format func(parent Visitable, flag *Flag) *Group) Option {
   264  	return PostBuild(func(kong *Kong) error {
   265  		parents := []Visitable{kong.Model}
   266  		return Visit(kong.Model, func(node Visitable, next Next) error {
   267  			if flag, ok := node.(*Flag); ok && flag.Group == nil {
   268  				flag.Group = format(parents[len(parents)-1], flag)
   269  			}
   270  			parents = append(parents, node)
   271  			defer func() { parents = parents[:len(parents)-1] }()
   272  			return next(nil)
   273  		})
   274  	})
   275  }
   276  
   277  // Groups associates `group` field tags with group metadata.
   278  //
   279  // This option is used to simplify Kong tags while providing
   280  // rich group information such as title and optional description.
   281  //
   282  // Each key in the "groups" map corresponds to the value of a
   283  // `group` Kong tag, while the first line of the value will be
   284  // the title, and subsequent lines if any will be the description of
   285  // the group.
   286  //
   287  // See also ExplicitGroups for a more structured alternative.
   288  type Groups map[string]string
   289  
   290  func (g Groups) Apply(k *Kong) error { //nolint: revive
   291  	for key, info := range g {
   292  		lines := strings.Split(info, "\n")
   293  		title := strings.TrimSpace(lines[0])
   294  		description := ""
   295  		if len(lines) > 1 {
   296  			description = strings.TrimSpace(strings.Join(lines[1:], "\n"))
   297  		}
   298  		k.groups = append(k.groups, Group{
   299  			Key:         key,
   300  			Title:       title,
   301  			Description: description,
   302  		})
   303  	}
   304  	return nil
   305  }
   306  
   307  // ExplicitGroups associates `group` field tags with their metadata.
   308  //
   309  // It can be used to provide a title or header to a command or flag group.
   310  func ExplicitGroups(groups []Group) Option {
   311  	return OptionFunc(func(k *Kong) error {
   312  		k.groups = groups
   313  		return nil
   314  	})
   315  }
   316  
   317  // UsageOnError configures Kong to display context-sensitive usage if FatalIfErrorf is called with an error.
   318  func UsageOnError() Option {
   319  	return OptionFunc(func(k *Kong) error {
   320  		k.usageOnError = fullUsage
   321  		return nil
   322  	})
   323  }
   324  
   325  // ShortUsageOnError configures Kong to display context-sensitive short
   326  // usage if FatalIfErrorf is called with an error. The default short
   327  // usage message can be overridden with kong.ShortHelp(...).
   328  func ShortUsageOnError() Option {
   329  	return OptionFunc(func(k *Kong) error {
   330  		k.usageOnError = shortUsage
   331  		return nil
   332  	})
   333  }
   334  
   335  // ClearResolvers clears all existing resolvers.
   336  func ClearResolvers() Option {
   337  	return OptionFunc(func(k *Kong) error {
   338  		k.resolvers = nil
   339  		return nil
   340  	})
   341  }
   342  
   343  // Resolvers registers flag resolvers.
   344  func Resolvers(resolvers ...Resolver) Option {
   345  	return OptionFunc(func(k *Kong) error {
   346  		k.resolvers = append(k.resolvers, resolvers...)
   347  		return nil
   348  	})
   349  }
   350  
   351  // IgnoreFields will cause kong.New() to skip field names that match any
   352  // of the provided regex patterns. This is useful if you are not able to add a
   353  // kong="-" struct tag to a struct/element before the call to New.
   354  //
   355  // Example: When referencing protoc generated structs, you will likely want to
   356  // ignore/skip XXX_* fields.
   357  func IgnoreFields(regexes ...string) Option {
   358  	return OptionFunc(func(k *Kong) error {
   359  		for _, r := range regexes {
   360  			if r == "" {
   361  				return errors.New("regex input cannot be empty")
   362  			}
   363  
   364  			re, err := regexp.Compile(r)
   365  			if err != nil {
   366  				return fmt.Errorf("unable to compile regex: %v", err)
   367  			}
   368  
   369  			k.ignoreFields = append(k.ignoreFields, re)
   370  		}
   371  
   372  		return nil
   373  	})
   374  }
   375  
   376  // ConfigurationLoader is a function that builds a resolver from a file.
   377  type ConfigurationLoader func(r io.Reader) (Resolver, error)
   378  
   379  // Configuration provides Kong with support for loading defaults from a set of configuration files.
   380  //
   381  // Paths will be opened in order, and "loader" will be used to provide a Resolver which is registered with Kong.
   382  //
   383  // Note: The JSON function is a ConfigurationLoader.
   384  //
   385  // ~ and variable expansion will occur on the provided paths.
   386  func Configuration(loader ConfigurationLoader, paths ...string) Option {
   387  	return OptionFunc(func(k *Kong) error {
   388  		k.loader = loader
   389  		for _, path := range paths {
   390  			f, err := os.Open(ExpandPath(path))
   391  			if err != nil {
   392  				if os.IsNotExist(err) || os.IsPermission(err) {
   393  					continue
   394  				}
   395  
   396  				return err
   397  			}
   398  			f.Close()
   399  
   400  			resolver, err := k.LoadConfig(path)
   401  			if err != nil {
   402  				return fmt.Errorf("%s: %v", path, err)
   403  			}
   404  			if resolver != nil {
   405  				k.resolvers = append(k.resolvers, resolver)
   406  			}
   407  		}
   408  		return nil
   409  	})
   410  }
   411  
   412  // ExpandPath is a helper function to expand a relative or home-relative path to an absolute path.
   413  //
   414  // eg. ~/.someconf -> /home/alec/.someconf
   415  func ExpandPath(path string) string {
   416  	if filepath.IsAbs(path) {
   417  		return path
   418  	}
   419  	if strings.HasPrefix(path, "~/") {
   420  		user, err := user.Current()
   421  		if err != nil {
   422  			return path
   423  		}
   424  		return filepath.Join(user.HomeDir, path[2:])
   425  	}
   426  	abspath, err := filepath.Abs(path)
   427  	if err != nil {
   428  		return path
   429  	}
   430  	return abspath
   431  }
   432  
   433  func siftStrings(ss []string, filter func(s string) bool) []string {
   434  	i := 0
   435  	ss = append([]string(nil), ss...)
   436  	for _, s := range ss {
   437  		if filter(s) {
   438  			ss[i] = s
   439  			i++
   440  		}
   441  	}
   442  	return ss[0:i]
   443  }
   444  
   445  // DefaultEnvars option inits environment names for flags.
   446  // The name will not generate if tag "env" is "-".
   447  // Predefined environment variables are skipped.
   448  //
   449  // For example:
   450  //
   451  //	--some.value -> PREFIX_SOME_VALUE
   452  func DefaultEnvars(prefix string) Option {
   453  	processFlag := func(flag *Flag) {
   454  		switch env := flag.Envs; {
   455  		case flag.Name == "help":
   456  			return
   457  		case len(env) == 1 && env[0] == "-":
   458  			flag.Envs = nil
   459  			return
   460  		case len(env) > 0:
   461  			return
   462  		}
   463  		replacer := strings.NewReplacer("-", "_", ".", "_")
   464  		names := append([]string{prefix}, camelCase(replacer.Replace(flag.Name))...)
   465  		names = siftStrings(names, func(s string) bool { return !(s == "_" || strings.TrimSpace(s) == "") })
   466  		name := strings.ToUpper(strings.Join(names, "_"))
   467  		flag.Envs = append(flag.Envs, name)
   468  		flag.Value.Tag.Envs = append(flag.Value.Tag.Envs, name)
   469  	}
   470  
   471  	var processNode func(node *Node)
   472  	processNode = func(node *Node) {
   473  		for _, flag := range node.Flags {
   474  			processFlag(flag)
   475  		}
   476  		for _, node := range node.Children {
   477  			processNode(node)
   478  		}
   479  	}
   480  
   481  	return PostBuild(func(k *Kong) error {
   482  		processNode(k.Model.Node)
   483  		return nil
   484  	})
   485  }
   486  
   487  // FlagNamer allows you to override the default kebab-case automated flag name generation.
   488  func FlagNamer(namer func(fieldName string) string) Option {
   489  	return OptionFunc(func(k *Kong) error {
   490  		k.flagNamer = namer
   491  		return nil
   492  	})
   493  }