Add configuration loading + docs + linter fixes.

This commit is contained in:
Alec Thomas
2018-06-13 10:33:22 +10:00
parent a5c97373ba
commit 232faad0a0
15 changed files with 218 additions and 17 deletions
+5
View File
@@ -12,8 +12,13 @@ jobs:
command: |
go get -v github.com/jstemmer/go-junit-report
go get -v -t -d ./...
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s latest
mkdir ~/report
when: always
- run:
name: Lint
command: |
./bin/golangci-lint run
- run:
name: Test
command: |
+33
View File
@@ -0,0 +1,33 @@
run:
tests: true
output:
print-issued-lines: false
linters:
enable-all: true
disable:
- maligned
linters-settings:
govet:
check-shadowing: true
gocyclo:
min-complexity: 10
dupl:
threshold: 100
goconst:
min-len: 5
min-occurrences: 3
gocyclo:
min-complexity: 20
issues:
max-per-linter: 0
max-same: 0
exclude-use-default: false
exclude:
- '^(G104|G204):'
# Very commonly not checked.
- 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked'
- 'exported method (.*\.MarshalJSON|.*\.UnmarshalJSON) should have comment or be unexported'
+22 -2
View File
@@ -10,6 +10,8 @@
1. [Commands and sub-commands](#commands-and-sub-commands)
1. [Supported tags](#supported-tags)
1. [Configuring Kong](#configuring-kong)
1. [`Configuration(loader, paths...)` - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files)
1. [`Resolver(...)` - support for default values from external sources](#resolver---support-for-default-values-from-external-sources)
1. [`*Mapper(...)` - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values)
1. [`Help(HelpFunc)` - customising help](#helphelpfunc---customising-help)
1. [`Hook(&field, HookFunc)` - callback hooks to execute when the command-line is parsed](#hookfield-hookfunc---callback-hooks-to-execute-when-the-command-line-is-parsed)
@@ -159,7 +161,25 @@ Both can coexist with standard Tag parsing.
## Configuring Kong
Each Kong parser can be configured via functional options passed to `New(cli interface{}, options...Option)`. The full set of options can be found in `options.go`.
Each Kong parser can be configured via functional options passed to `New(cli interface{}, options...Option)`.
The full set of options can be found in [options.go](https://github.com/alecthomas/kong/blob/master/options.go).
### `Configuration(loader, paths...)` - load defaults from configuration files
This option provides Kong with support for loading defaults from a set of configuration files. Each file is opened, if possible, and the loader called to create a resolver for that file.
eg.
```go
kong.Parse(&cli, kong.Configuration(kong.JSON, "/etc/myapp.json", "~/.myapp.json"))
```
### `Resolver(...)` - support for default values from external sources
Resolvers are Kong's extension point for providing default values from external sources. As an example, support for environment variables via the `env` tag is provided by a resolver. There's also a builtin resolver for JSON configuration files.
Example resolvers can be found in [resolver.go](https://github.com/alecthomas/kong/blob/master/resolver.go).
### `*Mapper(...)` - customising how the command-line is mapped to Go values
@@ -176,7 +196,7 @@ type Mapper interface {
}
```
All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`) have mapperss registered by default. Mappers for custom types can be added using `kong.??Mapper(...)` options. Mappers are applied to fields in four ways:
All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`) have mappers registered by default. Mappers for custom types can be added using `kong.??Mapper(...)` options. Mappers are applied to fields in four ways:
1. `NamedMapper(string, Mapper)` and using the tag key `type:"<name>"`.
2. `KindMapper(reflect.Kind, Mapper)`.
+30
View File
@@ -0,0 +1,30 @@
// Package kong aims to support arbitrarily complex command-line structures with as little developer effort as possible.
//
// Here's an example:
//
// shell rm [-f] [-r] <paths> ...
// shell ls [<paths> ...]
//
// This can be represented by the following command-line structure:
//
// package main
//
// import "github.com/alecthomas/kong"
//
// var CLI struct {
// Rm struct {
// Force bool `short:"f" help:"Force removal."`
// Recursive bool `short:"r" help:"Recursively remove files."`
//
// Paths []string `arg help:"Paths to remove." type:"path"`
// } `cmd help:"Remove files."`
//
// Ls struct {
// Paths []string `arg optional help:"Paths to list." type:"path"`
// } `cmd help:"List paths."`
// }
//
// func main() {
// kong.Parse(&CLI)
// }
package kong
+3
View File
@@ -19,6 +19,7 @@ func Parse(cli interface{}, options ...Option) string {
return cmd
}
// FatalIfErrorf terminates with an error message if err != nil.
func FatalIfErrorf(err error, args ...interface{}) {
if App == nil {
panic("call kong.Parse() before using kong.FatalIfErrorf()")
@@ -26,6 +27,7 @@ func FatalIfErrorf(err error, args ...interface{}) {
App.FatalIfErrorf(err, args...)
}
// Errorf writes a message to Kong.Stderr with the application name prefixed.
func Errorf(format string, args ...interface{}) {
if App == nil {
panic("call kong.Parse() before using kong.Errorf()")
@@ -33,6 +35,7 @@ func Errorf(format string, args ...interface{}) {
App.Errorf(format, args...)
}
// Printf writes a message to Kong.Stdout with the application name prefixed.
func Printf(format string, args ...interface{}) {
if App == nil {
panic("call kong.Parse() before using kong.Printf()")
+2 -2
View File
@@ -26,9 +26,9 @@ func guessWidth(w io.Writer) int {
if _, _, err := syscall.Syscall6(
syscall.SYS_IOCTL,
uintptr(fd),
uintptr(fd), // nolint: unconvert
uintptr(syscall.TIOCGWINSZ),
uintptr(unsafe.Pointer(&dimensions)),
uintptr(unsafe.Pointer(&dimensions)), // nolint: gas
0, 0, 0,
); err == 0 {
return int(dimensions[1])
+2 -2
View File
@@ -58,7 +58,7 @@ func New(grammar interface{}, options ...Option) (*Kong, error) {
before: map[reflect.Value]HookFunc{},
registry: NewRegistry().RegisterDefaults(),
help: PrintHelp,
resolvers: []ResolverFunc{EnvResolver()},
resolvers: []ResolverFunc{Envars()},
}
for _, option := range options {
@@ -123,7 +123,7 @@ func (k *Kong) Parse(args []string) (command string, err error) {
if err != nil {
return "", err
}
if err := k.applyHooks(ctx); err != nil {
if err = k.applyHooks(ctx); err != nil {
return "", err
}
if ctx.Error != nil {
+2
View File
@@ -112,6 +112,7 @@ func TestFlagSliceWithSeparator(t *testing.T) {
}
func TestArgSlice(t *testing.T) {
// nolint: govet
var cli struct {
Slice []int `arg`
Flag bool
@@ -124,6 +125,7 @@ func TestArgSlice(t *testing.T) {
}
func TestArgSliceWithSeparator(t *testing.T) {
// nolint: govet
var cli struct {
Slice []string `arg`
Flag bool
+10
View File
@@ -7,23 +7,29 @@ import (
"strings"
)
// Application is the root of the Kong model.
type Application struct {
Node
HelpFlag *Flag
}
// Argument represents a branching positional argument.
type Argument = Node
// Command represents a command in the CLI.
type Command = Node
// NodeType is an enum representing the type of a Node.
type NodeType int
// Node type enumerations.
const (
ApplicationNode NodeType = iota
CommandNode
ArgumentNode
)
// Node is a branch in the CLI. ie. a command or positional argument.
type Node struct {
Type NodeType
Parent *Node
@@ -37,6 +43,7 @@ type Node struct {
Argument *Value // Populated when Type is ArgumentNode.
}
// AllFlags returns flags from all ancestor branches encountered.
func (n *Node) AllFlags() (out [][]*Flag) {
if n.Parent != nil {
out = append(out, n.Parent.AllFlags()...)
@@ -186,6 +193,9 @@ func (v *Value) Apply(value reflect.Value) {
v.Set = true
}
// Reset this value to its default, either the zero value or the parsed result of its "default" tag.
//
// Does not include resolvers.
func (v *Value) Reset() error {
v.Value.Set(reflect.Zero(v.Value.Type()))
if v.Default != "" {
+50 -1
View File
@@ -2,10 +2,14 @@ package kong
import (
"io"
"os"
"os/user"
"path/filepath"
"reflect"
"strings"
)
// Options apply optional changes to the Kong application.
// An Option applies optional changes to the Kong application.
type Option func(k *Kong)
// ExitFunction overrides the function used to terminate. This is useful for testing or interactive use.
@@ -110,3 +114,48 @@ func Resolver(resolvers ...ResolverFunc) Option {
k.resolvers = append(k.resolvers, resolvers...)
}
}
// ConfigurationFunc is a function that builds a resolver from a file.
type ConfigurationFunc func(r io.Reader) (ResolverFunc, error)
// Configuration provides Kong with support for loading defaults from a set of configuration files.
//
// Paths will be opened in order, and "loader" will be used to provide a ResolverFunc which is registered with Kong.
//
// Note: The JSON function is a ConfigurationFunc.
//
// ~ expansion will occur on the provided paths.
func Configuration(loader ConfigurationFunc, paths ...string) Option {
return func(k *Kong) {
for _, path := range paths {
path = expandPath(path)
r, err := os.Open(path) // nolint: gas
if err != nil {
continue
}
resolver, err := loader(r)
if err == nil {
k.resolvers = append(k.resolvers, resolver)
}
_ = r.Close()
}
}
}
func expandPath(path string) string {
if filepath.IsAbs(path) {
return path
}
if strings.HasPrefix(path, "~/") {
user, err := user.Current()
if err != nil {
return path
}
return filepath.Join(user.HomeDir, path[2:])
}
abspath, err := filepath.Abs(path)
if err != nil {
return path
}
return abspath
}
+31
View File
@@ -1,6 +1,9 @@
package kong
import (
"encoding/json"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/require"
@@ -16,3 +19,31 @@ func TestOptions(t *testing.T) {
require.Nil(t, p.Stderr)
require.Nil(t, p.Exit)
}
func TestConfigLoading(t *testing.T) {
first, err := ioutil.TempFile("", "")
require.NoError(t, err)
defer first.Close()
defer os.Remove(first.Name())
second, err := ioutil.TempFile("", "")
require.NoError(t, err)
defer second.Close()
defer os.Remove(second.Name())
var cli struct {
Flag string `json:"flag,omitempty"`
}
cli.Flag = "first"
err = json.NewEncoder(first).Encode(&cli)
require.NoError(t, err)
cli.Flag = ""
err = json.NewEncoder(second).Encode(&cli)
require.NoError(t, err)
p := mustNew(t, &cli, Configuration(JSON, first.Name(), second.Name()))
_, err = p.Parse(nil)
require.NoError(t, err)
require.Equal(t, "first", cli.Flag)
}
+4 -4
View File
@@ -11,10 +11,10 @@ import (
// ResolverFunc resolves a Flag value from an external source.
type ResolverFunc func(context *Context, parent *Path, flag *Flag) (string, error)
// JSONResolver returns a Resolver that retrieves values from a JSON source.
// JSON returns a Resolver that retrieves values from a JSON source.
//
// Hyphens in flag names are replaced with underscores.
func JSONResolver(r io.Reader) (ResolverFunc, error) {
func JSON(r io.Reader) (ResolverFunc, error) {
values := map[string]interface{}{}
err := json.NewDecoder(r).Decode(&values)
if err != nil {
@@ -61,10 +61,10 @@ func jsonDecodeValue(sep rune, value interface{}) (string, error) {
return "", fmt.Errorf("unsupported JSON value %v (of type %T)", value, value)
}
// EnvResolver resolves flag values using the `env:"<name>"` tag. It ignores flags without this tag.
// Envars resolves flag values using the `env:"<name>"` tag. It ignores flags without this tag.
//
// This resolver is installed by default.
func EnvResolver() ResolverFunc {
func Envars() ResolverFunc {
return func(context *Context, parent *Path, flag *Flag) (string, error) {
if flag.Tag.Env == "" {
return "", nil
+7 -6
View File
@@ -30,7 +30,7 @@ func newEnvParser(t *testing.T, cli interface{}, env envMap) (*Kong, func()) {
return parser, restoreEnv
}
func TestEnvResolverFlagBasic(t *testing.T) {
func TestEnvarsFlagBasic(t *testing.T) {
var cli struct {
String string `env:"KONG_STRING"`
Slice []int `env:"KONG_SLICE"`
@@ -47,7 +47,7 @@ func TestEnvResolverFlagBasic(t *testing.T) {
require.Equal(t, []int{5, 2, 9}, cli.Slice)
}
func TestEnvResolverFlagOverride(t *testing.T) {
func TestEnvarsFlagOverride(t *testing.T) {
var cli struct {
Flag string `env:"KONG_FLAG"`
}
@@ -59,7 +59,7 @@ func TestEnvResolverFlagOverride(t *testing.T) {
require.Equal(t, "hello", cli.Flag)
}
func TestEnvResolverOnlyPopulateUsedBranches(t *testing.T) {
func TestEnvarsOnlyPopulateUsedBranches(t *testing.T) {
// nolint
var cli struct {
UnvisitedArg struct {
@@ -84,7 +84,7 @@ func TestEnvResolverOnlyPopulateUsedBranches(t *testing.T) {
require.Equal(t, 0, cli.UnvisitedCmd.Int)
}
func TestEnvResolverTag(t *testing.T) {
func TestEnvarsTag(t *testing.T) {
var cli struct {
Slice []int `env:"KONG_NUMBERS"`
}
@@ -96,7 +96,7 @@ func TestEnvResolverTag(t *testing.T) {
require.Equal(t, []int{5, 2, 9}, cli.Slice)
}
func TestJSONResolverBasic(t *testing.T) {
func TestJSONBasic(t *testing.T) {
var cli struct {
String string
Slice []int
@@ -111,7 +111,7 @@ func TestJSONResolverBasic(t *testing.T) {
"slice_with_commas": ["a,b", "c"]
}`
r, err := JSONResolver(strings.NewReader(json))
r, err := JSON(strings.NewReader(json))
require.NoError(t, err)
parser := mustNew(t, &cli, Resolver(r))
@@ -219,6 +219,7 @@ func TestLastResolverWins(t *testing.T) {
}
func TestResolverSatisfiesRequired(t *testing.T) {
// nolint: govet
var cli struct {
Int int `required`
}
+10
View File
@@ -21,6 +21,7 @@ const (
PositionalArgumentToken // <arg>
)
// Token created by Scanner.
type Token struct {
Value string
Type TokenType
@@ -42,10 +43,12 @@ func (t Token) String() string {
}
}
// IsEOL returns true if this Token is past the end of the line.
func (t Token) IsEOL() bool {
return t.Type == EOLToken
}
// IsAny returns true if the token's type is any of those provided.
func (t Token) IsAny(types ...TokenType) bool {
for _, typ := range types {
if t.Type == typ {
@@ -75,6 +78,7 @@ type Scanner struct {
args []Token
}
// Scan creates a new Scanner from args with untyped tokens.
func Scan(args ...string) *Scanner {
s := &Scanner{}
for _, arg := range args {
@@ -83,10 +87,12 @@ func Scan(args ...string) *Scanner {
return s
}
// Len returns the number of input arguments.
func (s *Scanner) Len() int {
return len(s.args)
}
// Pop the front token off the Scanner.
func (s *Scanner) Pop() Token {
if len(s.args) == 0 {
return Token{Type: EOLToken}
@@ -123,6 +129,7 @@ func (s *Scanner) PopUntil(predicate func(Token) bool) (values []string) {
return
}
// Peek at the next Token or return an EOLToken.
func (s *Scanner) Peek() Token {
if len(s.args) == 0 {
return Token{Type: EOLToken}
@@ -130,16 +137,19 @@ func (s *Scanner) Peek() Token {
return s.args[0]
}
// Push an untyped Token onto the front of the Scanner.
func (s *Scanner) Push(arg string) *Scanner {
s.PushToken(Token{Value: arg})
return s
}
// PushTyped pushes a typed token onto the front of the Scanner.
func (s *Scanner) PushTyped(arg string, typ TokenType) *Scanner {
s.PushToken(Token{Value: arg, Type: typ})
return s
}
// PushToken pushes a preconstructed Token onto the front of the Scanner.
func (s *Scanner) PushToken(token Token) *Scanner {
s.args = append([]Token{token}, s.args...)
return s
+7
View File
@@ -8,6 +8,7 @@ import (
"unicode/utf8"
)
// Tag represents the parsed state of Kong tags in a struct field tag.
type Tag struct {
Cmd bool
Arg bool
@@ -145,28 +146,34 @@ func parseTag(fv reflect.Value, ft reflect.StructField) *Tag {
return t
}
// Has returns true if the tag contained the given key.
func (t *Tag) Has(k string) bool {
_, ok := t.items[k]
return ok
}
// Get returns the value of the given tag.
func (t *Tag) Get(k string) (string, bool) {
s, ok := t.items[k]
return s, ok
}
// GetBool returns true if the given tag looks like a boolean truth string.
func (t *Tag) GetBool(k string) (bool, error) {
return strconv.ParseBool(t.items[k])
}
// GetFloat parses the given tag as a float64.
func (t *Tag) GetFloat(k string) (float64, error) {
return strconv.ParseFloat(t.items[k], 64)
}
// GetInt parses the given tag as an int64.
func (t *Tag) GetInt(k string) (int64, error) {
return strconv.ParseInt(t.items[k], 10, 64)
}
// GetRune parses the given tag as a rune.
func (t *Tag) GetRune(k string) (rune, error) {
r, _ := utf8.DecodeRuneInString(t.items[k])
if r == utf8.RuneError {