Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4.0.0-alpha.1 #113

Merged
merged 21 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: test
on:
pull_request:
types: [opened, synchronize]
push:
branches: [main]
schedule:
- cron: "0 12 1 * *" # first day of the month at 12:00

jobs:
test:
strategy:
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]

runs-on: ${{ matrix.platform }}

defaults:
run:
shell: bash

steps:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: 1.x

- name: Check out repo
uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Prepare cache
id: cache
run: |
echo "GOCACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT
echo "GOMODCACHE=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
echo "GOVERSION=$(go env GOVERSION)" >> $GITHUB_OUTPUT
mkdir -p $(go env GOCACHE) || true
mkdir -p $(go env GOMODCACHE) || true
- name: Cache
uses: actions/cache@v3
with:
path: |
${{ steps.cache.outputs.GOCACHE }}
${{ steps.cache.outputs.GOMODCACHE }}
key: test.1-${{ runner.os }}-${{ steps.cache.outputs.GOVERSION }}-${{ hashFiles('**/go.mod') }}
restore-keys: |
test.1-${{ runner.os }}-${{ steps.cache.outputs.GOVERSION }}-${{ hashFiles('**/go.mod') }}
test.1-${{ runner.os }}-${{ steps.cache.outputs.GOVERSION }}-
test.1-${{ runner.os }}-
- name: Install tools
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
go install mvdan.cc/gofumpt@latest
go install github.com/mgechev/revive@latest
- name: Run gofmt
if: matrix.platform != 'windows-latest' # :<
run: diff <(gofmt -d . 2>/dev/null) <(printf '')

- name: Run go vet
run: go vet ./...

- name: Run staticcheck
run: staticcheck ./...

- name: Run gofumpt
run: gofumpt -d -e -l .

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

- name: Run lint-parallel-tests
run: hack/lint-parallel-tests

- name: Run go test
run: go test -v -race ./...
37 changes: 0 additions & 37 deletions .github/workflows/test.yml

This file was deleted.

4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,15 @@
APPENDIX: How to apply the Apache License to your work.

To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright {yyyy} {name of copyright owner}
Copyright [yyyy] [name of copyright owner]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
206 changes: 112 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,131 +1,149 @@
# ff [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/peterbourgon/ff/v3) [![Latest Release](https://img.shields.io/github/v/release/peterbourgon/ff?style=flat-square)](https://github.com/peterbourgon/ff/releases/latest) ![Build Status](https://github.com/peterbourgon/ff/actions/workflows/test.yml/badge.svg?branch=main)
# ff [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/peterbourgon/ff/v4) [![Latest Release](https://img.shields.io/github/v/release/peterbourgon/ff?style=flat-square)](https://github.com/peterbourgon/ff/releases/latest) ![Build Status](https://github.com/peterbourgon/ff/actions/workflows/test.yaml/badge.svg?branch=main)

ff stands for flags-first, and provides an opinionated way to populate a
[flag.FlagSet](https://golang.org/pkg/flag#FlagSet) with configuration data from
the environment. By default, it parses only from the command line, but you can
enable parsing from environment variables (lower priority) and/or a
configuration file (lowest priority).
ff is a flags-first approach for programs to receive runtime configuration.

Building a commandline application in the style of `kubectl` or `docker`?
Consider [package ffcli](https://pkg.go.dev/github.com/peterbourgon/ff/v3/ffcli),
a natural companion to, and extension of, package ff.
As the name suggests, it's all based on flags. Every config parameter is
expected to be defined as a flag, to ensure that `myprogram -h` will reliably
describe the complete configuration surface area of the program.

Building a command-line application in the style of `kubectl` or `docker`?
[Command](#command) offers a declarative approach that may be simpler and easier
to maintain than common alternatives.

## Usage

Define a flag.FlagSet in your func main.
This module provides a getopts(3)-style flag set, which can be used as follows.

```go
import (
"flag"
"os"
"time"

"github.com/peterbourgon/ff/v3"
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")
_ = 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!

func main() {
fs := flag.NewFlagSet("my-program", flag.ContinueOnError)
var (
listenAddr = fs.String("listen-addr", "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)")
)
```go
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", "log debug information")
_ = fs.String("config", "", "config file (optional)")
)
```

Then, call ff.Parse instead of fs.Parse.
[Options](https://pkg.go.dev/github.com/peterbourgon/ff/v3#Option)
are available to control parse behavior.
Once you have a set of flags, use ff.Parse to parse it. Options can be provided
to influence parsing behavior.

```go
err := ff.Parse(fs, os.Args[1:],
ff.WithEnvVarPrefix("MY_PROGRAM"),
ff.WithConfigFileFlag("config"),
ff.WithConfigFileParser(ff.PlainParser),
)
err := ff.Parse(fs, os.Args[1:],
ff.WithEnvVarPrefix("MY_PROGRAM"),
ff.WithConfigFileFlag("config"),
ff.WithConfigFileParser(ff.PlainParser),
)
```

This example will parse flags from the commandline args, just like regular
package flag, with the highest priority. (The flag's default value will be used
only if the flag remains unset after parsing all provided sources of
configuration.)
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.

Additionally, the example will look in the environment for variables with a
`MY_PROGRAM` prefix. Flag names are capitalized, and separator characters are
converted to underscores. In this case, for example, `MY_PROGRAM_LISTEN_ADDR`
would match to `listen-addr`.
## Environment variables

Finally, if a `-config` file is specified, the example will try to parse it
using the PlainParser, which expects files in this format.
It's possible to take runtime configuration from env vars. The options
[WithEnvVars][withenvvars] and [WithEnvVarPrefix][withenvvarprefix] enable this
feature and determine how env var keys are mapped to flag 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)
var (
port = fs.Int("port", 8080, "listen port for server (also via PORT)")
debug = fs.Bool("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)
```
listen-addr localhost:8080
refresh 30s
debug true

```shell
$ env PORT=9090 myservice
port 9090, debug false
$ env PORT=9090 DEBUG=1 myservice --port=1234
port 1234, debug true
```

You could also use the JSONParser, which expects a JSON object.
## Config files

```json
{
"listen-addr": "localhost:8080",
"refresh": "30s",
"debug": true
}
```
It's also 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
.env config files, and also defines its own simple config file format.

Or, you could write your own config file parser.
[withconfigfile]: https://pkg.go.dev/github.com/peterbourgon/ff/v4#WithConfigFile
[withconfigfileflag]: https://pkg.go.dev/github.com/peterbourgon/ff/v4#WithConfigFileFlag
[withconfigfileparser]: https://pkg.go.dev/github.com/peterbourgon/ff/v4#WithConfigFileParser

```go
// ConfigFileParser interprets the config file represented by the reader
// and calls the set function for each parsed flag pair.
type ConfigFileParser func(r io.Reader, set func(name, value string) error) error
fs := flag.NewFlagSet("myservice", flag.ContinueOnError)
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")
)
ff.Parse(fs, os.Args[1:], ff.WithConfigFileFlag("config"), ff.WithConfigFileParser(ff.PlainParser))
fmt.Printf("port %d, debug %v\n", *port, *debug)
```

## Flags and env vars
```shell
$ printf "port 9090\n" >1.conf ; myservice --config=1.conf
port 9090, debug false
$ printf "port 9090\ndebug\n" >2.conf ; myservice --config=2.conf --port=1234
port 1234, debug true
```

One common use case is to allow configuration from both flags and env vars.
## Priority

```go
package main
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
"user" configuration.

import (
"flag"
"fmt"
"os"
Envioronment variables have the next-highest priority, because they reflect
configuration set in the runtime context -- we call env vars the "session"
configuration.

"github.com/peterbourgon/ff/v3"
)
Config files have the lowest priority, because they represent config that's
static to the host -- we call config files the "host" configuration.

func main() {
fs := flag.NewFlagSet("myservice", flag.ContinueOnError)
var (
port = fs.Int("port", 8080, "listen port for server (also via PORT)")
debug = fs.Bool("debug", false, "log debug information (also via DEBUG)")
)
if err := ff.Parse(fs, os.Args[1:], ff.WithEnvVars()); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}

fmt.Printf("port %d, debug %v\n", *port, *debug)
}
```
# Commands

```
$ env PORT=9090 myservice
port 9090, debug false
$ env PORT=9090 DEBUG=1 myservice -port=1234
port 1234, debug true
```
[Command][command] is a declarative and lightweight alternative to common CLI
frameworks like [spf13/cobra][cobra], [urfave/cli][urfave], or
[alecthomas/kingpin][kingpin].

[command]: https://pkg.go.dev/github.com/peterbourgon/ff/v4#Command
[cobra]: https://github.com/spf13/cobra
[urfave]: https://github.com/urfave/cli
[kingpin]: https://github.com/alecthomas/kingpin

## Error handling
Those frameworks have relatively large APIs, in order to support a large number
of "table stakes" features. In contrast, the command API is quite small, with
the immediate goal of being intuitive and productive, and the long-term goal of
producing CLI applications that are substantially easier to understand and
maintain.

In general, you should call flag.NewFlagSet with the flag.ContinueOnError error
handling strategy, which, somewhat confusingly, is the only way that ff.Parse can
return errors. (The other strategies terminate the program on error. Rude!) This
is [the only way to detect certain types of parse failures][90], in addition to
being good practice in general.
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 provided by the consumer.

[90]: https://github.com/peterbourgon/ff/issues/90
See [the examples directory](examples/) for sample CLI applications.
Loading