Skip to content

Commit

Permalink
Tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
peterbourgon committed Aug 29, 2023
1 parent 80cf439 commit 9d2e56d
Show file tree
Hide file tree
Showing 37 changed files with 2,671 additions and 746 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
run: gofumpt -d -e -l .

- name: Run revive
run: revive --exclude="./examples/..." ./...
run: revive --set_exit_status --exclude="./examples/..." ./...

- name: Run lint-parallel-tests
run: hack/lint-parallel-tests
Expand Down
46 changes: 23 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ fs := ff.NewFlags("myprogram")
var (
listenAddr = fs.StringLong("listen", "localhost:8080", "listen address")
refresh = fs.Duration('r', "refresh", 15*time.Second, "refresh interval")
debug = fs.Bool('d', "debug", "log debug information")
debug = fs.Bool('d', "debug", false, "log debug information")
_ = fs.StringLong("config", "", "config file (optional)")
)
```

You can also use a standard library flag set. If you do, be sure to use the
ContinueOnError error handling strategy. Other options either panic or terminate
the program on parse errors. Rude!
[flag.ContinueOnError] error handling strategy. Other options either panic or
terminate the program on parse errors. Rude!

```go
fs := flag.NewFlagSet("myprogram", flag.ContinueOnError)
Expand All @@ -38,8 +38,8 @@ var (
)
```

Once you have a set of flags, use ff.Parse to parse it. Options can be provided
to influence parsing behavior.
Once you have a set of flags, use [ff.Parse] to parse it. Options can be
provided to control parsing behavior.

```go
err := ff.Parse(fs, os.Args[1:],
Expand All @@ -50,24 +50,24 @@ err := ff.Parse(fs, os.Args[1:],
```

Flags are always set from the provided command-line arguments first. In the
above example, flags will also be set from env vars beginning with `MY_PROGRAM`.
Finally, if the user specifies a config file, flags will be set from values in
that file, as parsed by ff.PlainParser.
above example, flags will also be set from env vars beginning with `MY_PROGRAM`,
and then, if the user specifies a config file, from values in that file, as
parsed by [ff.PlainParser].

## Environment variables

It's possible to take runtime configuration from env vars. The options
It's possible to take runtime configuration from the environment. The options
[WithEnvVars][withenvvars] and [WithEnvVarPrefix][withenvvarprefix] enable this
feature and determine how env var keys are mapped to flag names.
feature and determine how flag names are mapped to environment variable names.

[withenvvars]: https://pkg.go.dev/github.com/peterbourgon/ff/v4#WithEnvVars
[withenvvarprefix]: https://pkg.go.dev/github.com/peterbourgon/ff/v4#WithEnvVarPrefix

```go
fs := flag.NewFlagSet("myservice", flag.ContinueOnError)
fs := ff.NewFlags("myservice")
var (
port = fs.Int("port", 8080, "listen port for server (also via PORT)")
debug = fs.Bool("debug", false, "log debug information (also via DEBUG)")
port = fs.Int('p', "port", 8080, "listen port for server (also via PORT)")
debug = fs.Bool('d', "debug", false, "log debug information (also via DEBUG)")
)
ff.Parse(fs, os.Args[1:], ff.WithEnvVars())
fmt.Printf("port %d, debug %v\n", *port, *debug)
Expand All @@ -82,7 +82,7 @@ port 1234, debug true

## Config files

It's also possible to take runtime configuration from config files. The options
It's possible to take runtime configuration from config files. The options
[WithConfigFile][withconfigfile], [WithConfigFileFlag][withconfigfileflag], and
[WithConfigFileParser][withconfigfileparser] control how config files are
specified and parsed. This module includes support for JSON, YAML, TOML, and
Expand All @@ -93,11 +93,11 @@ specified and parsed. This module includes support for JSON, YAML, TOML, and
[withconfigfileparser]: https://pkg.go.dev/github.com/peterbourgon/ff/v4#WithConfigFileParser

```go
fs := flag.NewFlagSet("myservice", flag.ContinueOnError)
fs := ff.NewFlags("myservice")
var (
port = fs.Int("port", 8080, "listen port for server (also via PORT)")
debug = fs.Bool("debug", false, "log debug information (also via DEBUG)")
_ = fs.String("config", "", "config file")
port = fs.IntLong("port", 8080, "listen port for server")
debug = fs.BoolLong("debug", false, "log debug information")
_ = fs.StringLong("config", "", "config file")
)
ff.Parse(fs, os.Args[1:], ff.WithConfigFileFlag("config"), ff.WithConfigFileParser(ff.PlainParser))
fmt.Printf("port %d, debug %v\n", *port, *debug)
Expand All @@ -113,15 +113,15 @@ port 1234, debug true
## Priority

Command-line args have the highest priority, because they're explicitly given to
each running instance of a program by the user -- we call command-line args the
each running instance of a program by the user. Think of command-line args as the
"user" configuration.

Envioronment variables have the next-highest priority, because they reflect
configuration set in the runtime context -- we call env vars the "session"
Environment variables have the next-highest priority, because they reflect
configuration set in the runtime context. Think of env vars as the "session"
configuration.

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

# Commands

Expand All @@ -146,4 +146,4 @@ 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 provided by the consumer.

See [the examples directory](examples/) for sample CLI applications.
See [the examples directory](examples/) for some CLI tools built with commands.
34 changes: 16 additions & 18 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ type Command struct {
// Required.
Name string

// Usage is a single line which should describe the syntax of the command,
// including flags and arguments. It's typically printed at the top of the
// help output for the command. For example,
// Usage is a single line string which should describe the syntax of the
// command, including flags and arguments. It's typically printed at the top
// of the help output for the command. For example,
//
// USAGE
// cmd [FLAGS] subcmd [FLAGS] <ARG> [<ARG>...]
Expand All @@ -44,8 +44,8 @@ type Command struct {
// output for the command, separate from other sections.
//
// Long help should be formatted for user readability. For example, if help
// output is written to a terminal, long help should hard-wrap lines at an
// appropriate column width for that terminal.
// output is written to a terminal, long help should include newlines which
// hard-wrap the string at an appropriate column width for that terminal.
//
// Optional.
LongHelp string
Expand Down Expand Up @@ -78,15 +78,14 @@ type Command struct {
// the terminal command during the parse phase. The args passed to Exec are
// the args left over after parsing.
//
// Optional. If not provided, and this command is identified as the terminal
// command during the parse phase, the run phase will return NoExecError.
Exec func(context.Context, []string) error
// Optional. If not provided, running this command will result in ErrNoExec.
Exec func(ctx context.Context, args []string) error
}

// Parse the args and options against the defined command, which sets relevant
// flags, traverses the command hierarchy to select a terminal command, and
// captures the arguments that will be given to that command's exec function.
// Args should not include the name of the program: os.Args[1:], not os.Args.
// The args should not include the program name: pass os.Args[1:], not os.Args.
func (cmd *Command) Parse(args []string, options ...Option) error {
// Initial validation and safety checks.
if cmd.Name == "" {
Expand Down Expand Up @@ -132,9 +131,9 @@ func (cmd *Command) Parse(args []string, options ...Option) error {
return nil
}

// Run the exec function of the command selected during the parse phase, passing
// the args left over after parsing. Calling run without first calling parse
// will result in an error.
// Run the Exec function of the terminal command selected during the parse
// phase, passing the args left over after parsing. Calling [Command.Run]
// without first calling [Command.Parse] will result in [ErrNotParsed].
func (cmd *Command) Run(ctx context.Context) error {
switch {
case !cmd.isParsed:
Expand All @@ -150,7 +149,7 @@ func (cmd *Command) Run(ctx context.Context) error {
}
}

// ParseAndRun calls parse and then, on success, run.
// ParseAndRun calls [Command.Parse] and, upon success, [Command.Run].
func (cmd *Command) ParseAndRun(ctx context.Context, args []string, options ...Option) error {
if err := cmd.Parse(args, options...); err != nil {
return fmt.Errorf("parse: %w", err)
Expand All @@ -164,7 +163,7 @@ func (cmd *Command) ParseAndRun(ctx context.Context, args []string, options ...O
}

// GetSelected returns the terminal command selected during the parse phase, or
// nil if the command hasn't been parsed.
// nil if the command hasn't been successfully parsed.
func (cmd *Command) GetSelected() *Command {
if cmd.selected == nil {
return nil
Expand All @@ -184,10 +183,9 @@ func (cmd *Command) GetParent() *Command {
return cmd.parent
}

// Reset every command in the command tree, including all flag sets, to their
// initial state. Flag sets must implement [Resetter], or else reset will return
// an error. After a successful reset, the command can be parsed and run as if
// it were newly constructed.
// Reset every command in the command tree to its initial state, including all
// flag sets. Every flag set must implement [Resetter], or else reset will
// return an error.
func (cmd *Command) Reset() error {
var check func(*Command) error

Expand Down
16 changes: 10 additions & 6 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
// Package ff provides a flags-first approach to runtime configuration.
//
// [Parse] populates a flag set with runtime configuration from environment
// variables and config files.
// [Parse] is the central function. It mirrors [flag.FlagSet.Parse] and
// populates a set of [Flags] from commandline arguments, environment variables,
// and/or a config file. [Option] values control parse behavior.
//
// [Command] can be used to build hierarchical command-line applications in a
// declarative style.
// [CoreFlags] is a standard, getopts(3)-inspired implementation of the [Flags]
// interface. Consumers can create a CoreFlags via [NewFlags], or adapt an
// existing [flag.FlagSet] to a CoreFlags via [NewStdFlags], or provide their
// own implementation altogether.
//
// [Flags] is the core interface of the package. Consumers may use the
// getopts(3)-inspired [CoreFlags] implementation, or provide their own.
// [Command] is provided as a way to build hierarchical CLI tools, like docker
// or kubectl, in a simple and declarative style. It's intended to be easier to
// understand and maintain than more common alternatives.
package ff
22 changes: 10 additions & 12 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,27 @@ import (
)

var (
// ErrHelp should be returned by the parse method of flag sets, when the
// provided args indicate the user has requested help. Usually this means
// -h or --help was one of the args.
// ErrHelp should be returned by flag sets during parse, when the provided
// args indicate the user has requested help.
ErrHelp = flag.ErrHelp

// ErrDuplicateFlag is returned by the core flag set, if a flag is added
// that has the same name as an existing flag.
// ErrDuplicateFlag should be returned by flag sets when the user tries to
// add a flag with the same name as a pre-existing flag.
ErrDuplicateFlag = errors.New("duplicate flag")

// ErrNotParsed may be returned by flag sets, when a method is called that
// requires the flag set to have been successfully parsed, but it hasn't
// been.
// ErrNotParsed may be returned by flag set methods which require the flag
// set to have been successfully parsed, and that condition isn't satisfied.
ErrNotParsed = errors.New("not parsed")

// 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.
ErrAlreadyParsed = errors.New("already parsed")

// ErrUnknownFlag should be returned by flag sets methods to indicate that a
// specific or user-requested flag was provided but could not be found.
ErrUnknownFlag = errors.New("unknown flag")

// ErrNoExec is returned when a command without an exec function is run.
ErrNoExec = errors.New("no exec function")

// ErrUnknownFlag should be returned by flag sets, when a specific or
// user-requested flag could not be found.
ErrUnknownFlag = errors.New("unknown flag")
)
2 changes: 1 addition & 1 deletion example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/peterbourgon/ff/v4"
)

func ExampleParse() {
func ExampleParse_args() {
fs := ff.NewFlags("myprogram")
var (
listen = fs.StringLong("listen", "localhost:8080", "listen address")
Expand Down
5 changes: 3 additions & 2 deletions examples/objectctl/cmd/objectctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/peterbourgon/ff/v4/examples/objectctl/pkg/listcmd"
"github.com/peterbourgon/ff/v4/examples/objectctl/pkg/objectapi"
"github.com/peterbourgon/ff/v4/examples/objectctl/pkg/rootcmd"
"github.com/peterbourgon/ff/v4/ffhelp"
)

func main() {
Expand Down Expand Up @@ -42,7 +43,7 @@ func exec(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io
)

if err := root.Command.Parse(args); err != nil {
fmt.Fprintf(stderr, "\n%s\n\n", ff.DefaultCommandUsage(root.Command))
fmt.Fprintf(stderr, "%s\n", ffhelp.CommandHelp(root.Command))
return fmt.Errorf("parse: %w", err)
}

Expand All @@ -54,7 +55,7 @@ func exec(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io
root.Client = client

if err := root.Command.Run(ctx); err != nil {
fmt.Fprintf(stderr, "\n%s\n\n", ff.DefaultCommandUsage(root.Command))
fmt.Fprintf(stderr, "%s\n", ffhelp.CommandHelp(root.Command))
return fmt.Errorf("run: %w", err)
}

Expand Down
12 changes: 5 additions & 7 deletions examples/objectctl/cmd/objectctl/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,7 @@ func TestExec(t *testing.T) {
}

const rootUsage = `
COMMAND
objectctl
objectctl
USAGE
objectctl [FLAGS] <SUBCOMMAND> ...
Expand All @@ -109,22 +108,21 @@ SUBCOMMANDS
FLAGS
--token STRING secret token for object API
-v, --verbose log verbose output (default: false)
-v, --verbose log verbose output
`

const listUsage = `
COMMAND
list -- list available objects
list -- list available objects
USAGE
objectctl list [FLAGS]
FLAGS
-a, --atime include last access time of each object (default: false)
-a, --atime include last access time of each object
FLAGS (objectctl)
--token STRING secret token for object API
-v, --verbose log verbose output (default: false)
-v, --verbose log verbose output
`

const listOutput = `
Expand Down
8 changes: 7 additions & 1 deletion examples/objectctl/pkg/createcmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/peterbourgon/ff/v4"
"github.com/peterbourgon/ff/v4/examples/objectctl/pkg/rootcmd"
"github.com/peterbourgon/ff/v4/ffval"
)

type CreateConfig struct {
Expand All @@ -21,7 +22,12 @@ func New(rootConfig *rootcmd.RootConfig) *CreateConfig {
var cfg CreateConfig
cfg.RootConfig = rootConfig
cfg.Flags = ff.NewFlags("create").SetParent(cfg.RootConfig.Flags)
cfg.Flags.BoolVar(&cfg.Overwrite, 0, "overwrite", false, "overwrite an existing object")
cfg.Flags.AddFlag(ff.CoreFlagConfig{
LongName: "overwrite",
Value: ffval.NewValue(&cfg.Overwrite),
Usage: "overwrite an existing object",
NoDefault: true,
})
cfg.Command = &ff.Command{
Name: "create",
Usage: "objectctl create [FLAGS] <KEY> <VALUE>",
Expand Down
10 changes: 8 additions & 2 deletions examples/objectctl/pkg/deletecmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/peterbourgon/ff/v4"
"github.com/peterbourgon/ff/v4/examples/objectctl/pkg/rootcmd"
"github.com/peterbourgon/ff/v4/ffval"
)

type DeleteConfig struct {
Expand All @@ -20,10 +21,15 @@ func New(parent *rootcmd.RootConfig) *DeleteConfig {
var cfg DeleteConfig
cfg.RootConfig = parent
cfg.Flags = ff.NewFlags("delete").SetParent(parent.Flags)
cfg.Flags.BoolVar(&cfg.Force, 0, "force", false, "force delete")
cfg.Flags.AddFlag(ff.CoreFlagConfig{
LongName: "force",
Value: ffval.NewValue(&cfg.Force),
Usage: "force delete",
NoDefault: true,
})
cfg.Command = &ff.Command{
Name: "delete",
Usage: "objectctl delete <KEY>",
Usage: "objectctl delete [FLAGS] <KEY>",
ShortHelp: "delete an object",
Flags: cfg.Flags,
Exec: cfg.Exec,
Expand Down
Loading

0 comments on commit 9d2e56d

Please sign in to comment.