ff configuration library

repository·main·Indexed 23 days ago

https://github.com/peterbourgon/ff

A flags-first configuration library for Go that allows developers to define configuration parameters as flags and parse them from command-line arguments, environment variables, and configuration files. It provides a feature-rich ff.FlagSet supporting short and long flag names, as well as a declarative ff.Command system for building complex CLI applications with subcommands and inherited global flags.

Tokens
7.5K
Snippets
10
Records
38
Agent score
80%

What's inside ff

  1. Understand configuration parse priority

    main

    When using ff, configuration is resolved using the following priority (from highest to lowest):

    1. Command-line arguments: Highest priority (user configuration).
    2. Environment variables: Medium priority (session configuration).
    3. Config files: Lowest priority (host configuration).
  2. How parent flag sets work in ff

    main

    An ff.FlagSet can have a parent flag set using the .SetParent(parentfs) method. This allows a "child" flag set to inherit and parse all flags defined in the parent set, in addition to its own. This is useful for defining global flags (like --log or --config) that should be available to all subcommands.

    parentfs := ff.NewFlagSet("parentcommand")
    var (
    	loglevel = parentfs.StringEnum('l', "log", "log level: debug, info, error", "info", "debug", "error")
    	_        = parentfs.StringLong("config", "", "config file (optional)")
    )
    
    childfs := ff.NewFlagSet("childcommand").SetParent(parentfs)
    var (
    	compress  = childfs.Bool('c', "compress", "enable compression")
    	transform = childfs.Bool('t', "transform", "enable transformation")
    	refresh   = childfs.DurationLong('r', "refresh", 15*time.Second, "refresh interval")
    )
    
    ff.Parse(childfs, os.Args[1:],
    	ff.WithEnvVarPrefix("MY_PROGRAM"),
    	ff.WithConfigFileFlag("config"),
    	ff.WithConfigFileParser(ff.PlainParser),
    )
  3. Use ff.FlagSet for advanced flag features

    main

    For a more feature-rich experience, use ff.NewFlagSet. It is inspired by getopts(3) and provides:

    • Short (-f) and long (--foo) flag names.
    • Useful flag types like StringSet, StringEnum, and DurationLong.
    • Improved parsing ergonomics.

    Example of defining various flag types with ff.FlagSet:

    fs := ff.NewFlagSet("myprogram")
    var (
    	addrs     = fs.StringSet('a', "addr", "remote address (repeatable)")
    	compress  = fs.Bool('c', "compress", "enable compression")
    	transform = fs.Bool('t', "transform", "enable transformation")
    	loglevel  = fs.StringEnum('l', "log", "log level: debug, info, error", "info", "debug", "error")
    	_         = fs.StringLong("config", "", "config file (optional)")
    )
    
    ff.Parse(fs, os.Args[1:],
    	ff.WithEnvVarPrefix("MY_PROGRAM"),
    	ff.WithConfigFileFlag("config"),
    	ff.WithConfigFileParser(ff.PlainParser),
    )
    
    fmt.Printf("addrs=%v compress=%v transform=%v loglevel=%v\n", *addrs, *compress, *transform, *loglevel)
  4. Parse a standard flag.FlagSet with ff

    main

    You can use ff.Parse to extend the standard library's flag.FlagSet with support for environment variables and configuration files. This allows you to maintain compatibility with standard Go flag definitions while adding multi-source configuration capabilities.

    Use ff.WithEnvVarPrefix to define the prefix for environment variables and ff.WithConfigFileFlag to specify which flag identifies the configuration file. Use ff.WithConfigFileParser to choose a parser (e.g., ff.PlainParser).

    fs := flag.NewFlagSet("myprogram", flag.ContinueOnError)
    var (
    	listenAddr = fs.String("listen", "localhost:8080", "listen address")
    	refresh    = fs.Duration("refresh", 15*time.Second, "refresh interval")
    	debug      = fs.Bool("debug", false, "log debug information")
    	_          = fs.String("config", "", "config file (optional)")
    )
    
    ff.Parse(fs, os.Args[1:],
    	ff.WithEnvVarPrefix("MY_PROGRAM"),
    	ff.WithConfigFileFlag("config"),
    	ff.WithConfigFileParser(ff.PlainParser),
    )
    
    fmt.Printf("listen=%s refresh=%s debug=%v\n", *listen, *refresh, *debug)
  5. Build a CLI with subcommands using ff.Command

    main

    The ff.Command type provides a declarative way to build complex CLI applications with subcommands (similar to docker or kubectl).

    To build a command tree:

    1. Define a root ff.Command with its own ff.FlagSet.
    2. Define subcommands as ff.Command instances.
    3. Use .SetParent(parentFlagSet) on subcommand flag sets to inherit global flags.
    4. Append subcommands to the Subcommands slice of the parent command.
    5. Call cmd.ParseAndRun(ctx, args) to execute the tree.
    // textctl -- root command
    textctlFlags := ff.NewFlagSet("textctl")
    verbose := textctlFlags.Bool('v', "verbose", "increase log verbosity")
    textctlCmd := &ff.Command{
    	Name:  "textctl",
    	Usage: "textctl [FLAGS] SUBCOMMAND ...",
    	Flags: textctlFlags,
    }
    
    // textctl repeat -- subcommand
    repeatFlags := ff.NewFlagSet("repeat").SetParent(textctlFlags)
    n := repeatFlags.IntShort('n', 3, "how many times to repeat")
    repeatCmd := &ff.Command{
    	Name:      "repeat",
    	Usage:     "textctl repeat [-n TIMES] ARG",
    	ShortHelp: "repeatedly print the first argument to stdout",
    	Flags:     repeatFlags,
    	Exec:      func(ctx context.Context, args []string) error { /* ... */ },
    }
    textctlCmd.Subcommands = append(textctlCmd.Subcommands, repeatCmd)
    
    if err := textctlCmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
    	fmt.Fprintf(os.Stderr, "%s\n", ffhelp.Command(textctlCmd))
    	fmt.Fprintf(os.Stderr, "error: %v\n", err)
    	os.Exit(1)
    }
  6. Generate help text with ffhelp

    main

    Unlike the standard flag.FlagSet, ff.FlagSet does not automatically print help text to os.Stderr on error. Instead, you should check the error returned by ff.Parse and use the ffhelp package to generate and print help text manually.

    • Use ffhelp.Flags(fs) to generate help text for a specific FlagSet.
    • Use ffhelp.Command(cmd) to generate help text for an ff.Command tree.
    if err := ff.Parse(childfs, os.Args[1:],
    	ff.WithEnvVarPrefix("MY_PROGRAM"),
    	ff.WithConfigFileFlag("config"),
    	ff.WithConfigFileParser(ff.PlainParser),
    ); err != nil {
    	fmt.Printf("%s\n", ffhelp.Flags(childfs))
    	fmt.Printf("err=%v\n", err)
    } else {
    	fmt.Printf("loglevel=%v compress=%v transform=%v refresh=%v\n", *loglevel, *compress, *transform, *refresh)
    }
  7. Use ff for flags-first runtime configuration

    main

    The ff package provides a way to populate configuration from multiple sources: command-line arguments, environment variables, and/or configuration files. The primary entry point is the Parse function.

    To use ff, you can pass either an implementation of the Flags interface or a standard flag.FlagSet to Parse. You can also provide Option values to customize the parsing behavior.

  8. How ff.FlagSet works

    main
    The FlagSet type is a standard implementation of the Flags interface. It is inspired by getopts(3) and supports both single-character flags (e.g., -f) and long-form flags (e.g., --foo).
  9. Use parent flag sets for hierarchical flags

    main
    You can create a hierarchy of flag sets using SetParent(parent *FlagSet). When a child flag set is parsed, it will also match against all flags defined in its parent(s), recursively. This is useful for subcommands or shared global flags.
  10. Build CLI applications with ff.Command

    main
    The Command type is a tool designed for building complex CLI applications (similar to docker or kubectl) using a simple and declarative style. It is intended to be more maintainable and easier to understand than many common CLI frameworks.
  11. How ff priority and configuration works

    main

    The ff package implements a layered configuration model. When Parse is called, it processes sources in a specific order of precedence. This allows users to provide defaults in a config file, override them with environment variables for containerized environments, and finally override everything with explicit command-line flags.

    Priority Order

    1. Command-line Arguments: The most specific source. If a flag is provided here, it is marked as provided and cannot be changed by other sources.
    2. Environment Variables: If enabled via options, ff looks for environment variables matching the flag's name (transformed to uppercase and using underscores for separators). If a flag was already set by the command line, environment variables are ignored for that flag.
    3. Config Files: If enabled, ff reads the file using a provided configParseFunc. If a flag was already set by the command line or environment, the config file value is ignored (unless the config file specifies the same flag multiple times, in which case it may append to slices).

    Environment Variable Mapping

    By default, ff transforms flag names into environment variable keys:

    • Hyphens - and dots . are replaced with underscores _.
    • The name is converted to uppercase.
    • An optional prefix can be added.

    Example: A flag --database-url becomes DATABASE_URL (or APP_DATABASE_URL if the APP prefix is used).

  12. How command hierarchy and selection works

    main

    The ff.Command structure models a tree. When Parse is called:

    1. It parses the flags for the current command.
    2. It looks at the first remaining argument (the first non-flag argument).
    3. It performs a case-insensitive comparison of that argument against the Name of all available Subcommands.
    4. If a match is found, it descends into that subcommand and repeats the process.
    5. If no match is found, the current command is marked as the selected terminal command.

    This allows for nested command structures like git remote add ... where remote is a subcommand of git, and add is a subcommand of remote.