Categorygithub.com/peterbourgon/ff/v4
modulepackage
4.0.0-alpha.4
Repository: https://github.com/peterbourgon/ff.git
Documentation: pkg.go.dev

# README

ff go.dev reference Latest Release Build Status

ff is a flags-first approach to configuration.

The basic idea is that myprogram -h should always show the complete configuration "surface area" of a program. Therefore, every config parameter should be defined as a flag. This module provides a simple and robust way to define those flags, and to parse them from command-line arguments, environment variables, and/or config files.

Building a command-line application in the style of kubectl or docker? ff.Command is a declarative approach that's simpler to write, and easier to maintain, than many common alternatives.

Usage

Parse a flag.FlagSet

Parse a flag.FlagSet from commandline args, env vars, and/or a config file, by using ff.Parse instead of flag.FlagSet.Parse, and passing relevant options to control parse behavior.

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)
$ myprogram -listen=localhost:9090
listen=localhost:9090 refresh=15s debug=false

$ env MY_PROGRAM_DEBUG=1 myprogram
listen=localhost:8080 refresh=15s debug=true

$ printf 'refresh 30s \n debug \n' > my.conf
$ myprogram -config=my.conf
listen=localhost:8080 refresh=30s debug=true

Upgrade to an ff.FlagSet

Alternatively, you can use the getopts(3)-inspired [ff.FlagSet][flagset], which provides short (-f) and long (--foo) flag names, more useful flag types, and other niceities.

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)
$ env MY_PROGRAM_LOG=debug myprogram -afoo -a bar --addr=baz --addr qux -ct
addrs=[foo bar baz qux] compress=true transform=true loglevel=debug

Parent flag sets

ff.FlagSet supports the notion of a parent flag set, which allows a "child" flag set to successfully parse every "parent" flag.

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("refresh", 15*time.Second, "refresh interval")
)

ff.Parse(childfs, os.Args[1:],
	ff.WithEnvVarPrefix("MY_PROGRAM"),
	ff.WithConfigFileFlag("config"),
	ff.WithConfigFileParser(ff.PlainParser),
)

fmt.Printf("loglevel=%v compress=%v transform=%v refresh=%v\n", *loglevel, *compress, *transform, *refresh)
$ myprogram --log=debug --refresh=1s
loglevel=debug compress=false transform=false refresh=1s

$ printf 'log error \n refresh 5s \n' > my.conf
$ myprogram --config my.conf
loglevel=error compress=false transform=false refresh=5s

Help output

Unlike flag.FlagSet, ff.FlagSet doesn't emit help text to os.Stderr as an invisible side effect of a failed parse. When using an ff.FlagSet, callers are expected to check the error returned by parse, and to emit help text to the user as appropriate. Package ffhelp provides functions that produce help text in a standard format, and tools for creating your own help text format.

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("refresh", 15*time.Second, "refresh interval")
)

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)
}
$ childcommand -h
NAME
  childcommand

FLAGS (childcommand)
  -c, --compress           enable compression
  -t, --transform          enable transformation
      --refresh DURATION   refresh interval (default: 15s)

FLAGS (parentcommand)
  -l, --log STRING         log level: debug, info, error (default: info)
      --config STRING      config file (optional)

err=parse args: flag: help requested

Parse priority

Command-line args have the highest priority, because they're explicitly provided to the program by the user. Think of command-line args as the "user" configuration.

Environment variables have the next-highest priority, because they represent configuration in the runtime environment. Think of env vars as the "session" configuration.

Config files have the lowest priority, because they represent config that's static to the host. Think of config files as the "host" configuration.

ff.Command

ff.Command is a tool for building larger CLI programs with sub-commands, like docker or kubectl. It's a declarative and lightweight alternative to more common frameworks like spf13/cobra, urfave/cli, or alecthomas/kingpin.

Commands are concerned only with the core mechanics of defining a command tree, parsing flags, and selecting a command to run. They're not intended to be a one-stop-shop for everything a command-line application may need. Features like tab completion, colorized output, etc. are orthogonal to command tree parsing, and can be easily added on top.

Here's a simple example of a basic command 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) // <-- set parent flag set
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) // <-- append to parent subcommands

// ...

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)
}

More sophisticated programs are available in the examples directory.

# Packages

Package ffenv provides an .env config file parser.
Package ffhelp provides tools to produce help text for flags and commands.
Package ffjson provides a JSON config file parser.
Package fftest provides tools for testing flag sets.
Package fftoml provides a TOML config file parser.
Package ffval provides common flag value types and helpers.
Package ffyaml provides a YAML config file parser.

# Functions

NewFlagSet returns a new flag set with the given name.
NewFlagSetFrom is a helper method that calls [NewFlagSet] with name, and then [FlagSet.AddStruct] with val, which must be a pointer to a struct.
Parse the flag set with the provided args.
PlainParser is a parser for config files in an extremely simple format.
WithConfigAllowMissingFile tells [Parse] to ignore config files that are specified but don't exist.
WithConfigFile tells [Parse] to read the provided filename as a config file.
WithConfigFileFlag tells [Parse] to treat the flag with the given name as a config file.
WithConfigFileParser tells [Parse] how to interpret a config file.
WithConfigIgnoreUndefinedFlags tells [Parse] to ignore flags in config files which are not defined in the parsed flag set.
WithEnvVarPrefix is like [WithEnvVars], but only considers environment variables beginning with the given prefix followed by an underscore.
WithEnvVars tells [Parse] to set flags from environment variables.
WithEnvVarSplit tells [Parse] to split environment variable values on the given delimiter, and to set the flag multiple times, once for each delimited token.
WithFilesystem tells [Parse] to use the provided filesystem when accessing files on disk, typically when reading a config file.

# Variables

ErrAlreadyParsed may be returned by the parse method of flag sets, if the flag set has already been successfully parsed, and cannot be parsed again.
ErrDuplicateFlag should be returned by flag sets when the user tries to add a flag with the same name as a pre-existing flag.
ErrHelp should be returned by flag sets during parse, when the provided args indicate the user has requested help.
ErrNoExec is returned when a command without an exec function is run.
ErrNotParsed may be returned by flag set methods which require the flag set to have been successfully parsed, and that condition isn't satisfied.
ErrUnknownFlag should be returned by flag sets methods to indicate that a specific or user-requested flag was provided but could not be found.

# Structs

Command is a declarative structure that combines a main function with a flag set and zero or more subcommands.
FlagConfig collects the required config for a flag in a flag set.
FlagSet is a standard implementation of [Flags].
ParseContext receives and maintains parse options.

# Interfaces

Flag describes a single runtime configuration parameter, defined in a set of [Flags], and with a value that can be parsed from a string.
Flags describes a collection of flags, typically associated with a specific command (or sub-command) executed by an end user.
Resetter may optionally be implemented by [Flags].

# Type aliases

ConfigFileParseFunc is a function that consumes the provided reader as a config file, and calls the provided set function for every name=value pair it discovers.
FlagSetAny must be either a [Flags] interface, or a concrete [*flag.FlagSet].
Option controls some aspect of parsing behavior.