Add configuration loading + docs + linter fixes.
This commit is contained in:
@@ -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: |
|
||||
|
||||
@@ -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'
|
||||
@@ -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)`.
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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])
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user