Add ConfigFlag for loading configuration through a flag.
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
1. [Command handling](#command-handling)
|
||||
1. [Switch on the command string](#switch-on-the-command-string)
|
||||
1. [Attach a `Run(...) error` method to each command](#attach-a-run-error-method-to-each-command)
|
||||
1. [BeforeHook\(\), AfterHook\(\) and the Bind\(\) option](#beforehook-afterhook-and-the-bind-option)
|
||||
1. [BeforeApply\(\), AfterApply\(\) and the Bind\(\) option](#BeforeApply-AfterApply-and-the-bind-option)
|
||||
1. [Flags](#flags)
|
||||
1. [Commands and sub-commands](#commands-and-sub-commands)
|
||||
1. [Branching positional arguments](#branching-positional-arguments)
|
||||
@@ -212,12 +212,11 @@ func main() {
|
||||
|
||||
```
|
||||
|
||||
## BeforeHook(), AfterHook() and the Bind() option
|
||||
## Hooks: BeforeResolve(), BeforeSet(), AfterSet() and the Bind() option
|
||||
|
||||
If a node in the grammar has a `BeforeHook(...) error` and/or `AfterHook(...) error` method, those methods will
|
||||
be called before validation/assignment and after validation/assignment, respectively.
|
||||
If a node in the grammar has a `BeforeResolve(...)`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those methods will be called before validation/assignment and after validation/assignment, respectively.
|
||||
|
||||
The `--help` flag is implemented with a `BeforeHook`.
|
||||
The `--help` flag is implemented with a `BeforeApply` hook.
|
||||
|
||||
Arguments to hooks are provided via the `Bind(...)` option. `*Kong`, `*Context` and `*Path` are also bound.
|
||||
|
||||
@@ -227,7 +226,7 @@ eg.
|
||||
// A flag with a hook that, if triggered, will set the debug loggers output to stdout.
|
||||
var debugFlag bool
|
||||
|
||||
func (d debugFlag) BeforeHook(logger *log.Logger) error {
|
||||
func (d debugFlag) BeforeApply(logger *log.Logger) error {
|
||||
logger.SetOutput(os.Stdout)
|
||||
return nil
|
||||
}
|
||||
@@ -493,7 +492,7 @@ The default help output is usually sufficient, but if not there are two solution
|
||||
|
||||
### `Bind(...)` - bind values for callback hooks and Run() methods
|
||||
|
||||
See the [section on hooks](#beforehook-afterhook-and-the-bind-option) for details.
|
||||
See the [section on hooks](#BeforeApply-AfterApply-and-the-bind-option) for details.
|
||||
|
||||
### Other options
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ type VersionFlag string
|
||||
|
||||
func (v VersionFlag) Decode(ctx *kong.DecodeContext) error { return nil }
|
||||
func (v VersionFlag) IsBool() bool { return true }
|
||||
func (v VersionFlag) BeforeHook(app *kong.Kong, vars kong.Vars) error {
|
||||
func (v VersionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error {
|
||||
fmt.Println(vars["version"])
|
||||
app.Exit(0)
|
||||
return nil
|
||||
|
||||
+29
-13
@@ -50,15 +50,16 @@ type Context struct {
|
||||
// Error that occurred during trace, if any.
|
||||
Error error
|
||||
|
||||
values map[*Value]reflect.Value // Temporary values during tracing.
|
||||
scan *Scanner
|
||||
values map[*Value]reflect.Value // Temporary values during tracing.
|
||||
resolvers []ResolverFunc // Extra context-specific resolvers.
|
||||
scan *Scanner
|
||||
}
|
||||
|
||||
// Trace path of "args" through the gammar tree.
|
||||
// Trace path of "args" through the grammar tree.
|
||||
//
|
||||
// The returned Context will include a Path of all commands, arguments, positionals and flags.
|
||||
//
|
||||
// Note that this will not modify the target grammar. Call Apply() to do so.
|
||||
// Call Resolve() after this, then finally Apply() to write parsed values into the target grammar.
|
||||
func Trace(k *Kong, args []string) (*Context, error) {
|
||||
c := &Context{
|
||||
Kong: k,
|
||||
@@ -70,7 +71,7 @@ func Trace(k *Kong, args []string) (*Context, error) {
|
||||
scan: Scan(args...),
|
||||
}
|
||||
c.Error = c.trace(c.Model.Node)
|
||||
return c, c.traceResolvers()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Value returns the value for a particular path element.
|
||||
@@ -173,14 +174,25 @@ func (c *Context) Command() string {
|
||||
return strings.Join(command, " ")
|
||||
}
|
||||
|
||||
// FlagValue returns the set value of a flag, if it was encountered and exists.
|
||||
func (c *Context) FlagValue(flag *Flag) reflect.Value {
|
||||
// AddResolver adds a context-specific resolver.
|
||||
//
|
||||
// This is most useful in the BeforeResolve() hook.
|
||||
func (c *Context) AddResolver(resolver ResolverFunc) {
|
||||
c.resolvers = append(c.resolvers, resolver)
|
||||
}
|
||||
|
||||
// FlagValue returns the set value of a flag if it was encountered and exists.
|
||||
func (c *Context) FlagValue(flag *Flag) interface{} {
|
||||
for _, trace := range c.Path {
|
||||
if trace.Flag == flag {
|
||||
return c.values[trace.Flag.Value]
|
||||
v, ok := c.values[trace.Flag.Value]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return v.Interface()
|
||||
}
|
||||
}
|
||||
return reflect.Value{}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recursively reset values to defaults (as specified in the grammar) or the zero value.
|
||||
@@ -363,9 +375,13 @@ func findPotentialCandidates(needle string, haystack []string, format string, ar
|
||||
return fmt.Errorf("%s", prefix)
|
||||
}
|
||||
|
||||
// Walk through flags from existing nodes in the path.
|
||||
func (c *Context) traceResolvers() error {
|
||||
if len(c.resolvers) == 0 {
|
||||
// Resolve walks through the traced path, applying resolvers to any unset flags.
|
||||
func (c *Context) Resolve() error {
|
||||
// Combine application-level resolvers and context resolvers.
|
||||
resolvers := []ResolverFunc{}
|
||||
resolvers = append(resolvers, c.Kong.resolvers...)
|
||||
resolvers = append(resolvers, c.resolvers...)
|
||||
if len(resolvers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -376,7 +392,7 @@ func (c *Context) traceResolvers() error {
|
||||
if _, ok := c.values[flag.Value]; ok {
|
||||
continue
|
||||
}
|
||||
for _, resolver := range c.resolvers {
|
||||
for _, resolver := range resolvers {
|
||||
s, err := resolver(c, path, flag)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package kong
|
||||
|
||||
// BeforeResolve is a documentation-only interface describing hooks that run before values are set.
|
||||
type BeforeResolve interface {
|
||||
// This is not the correct signature - see README for details.
|
||||
BeforeResolve(args ...interface{}) error
|
||||
}
|
||||
|
||||
// BeforeApply is a documentation-only interface describing hooks that run before values are set.
|
||||
type BeforeApply interface {
|
||||
// This is not the correct signature - see README for details.
|
||||
BeforeApply(args ...interface{}) error
|
||||
}
|
||||
|
||||
// AfterApply is a documentation-only interface describing hooks that run after values are set.
|
||||
type AfterApply interface {
|
||||
// This is not the correct signature - see README for details.
|
||||
AfterApply(args ...interface{}) error
|
||||
}
|
||||
@@ -43,6 +43,7 @@ type Kong struct {
|
||||
Stderr io.Writer
|
||||
|
||||
bindings bindings
|
||||
loader ConfigurationFunc
|
||||
resolvers []ResolverFunc
|
||||
registry *Registry
|
||||
|
||||
@@ -161,7 +162,7 @@ func mergeVars(base, extra map[string]string) map[string]string {
|
||||
|
||||
type helpValue bool
|
||||
|
||||
func (h helpValue) BeforeHook(ctx *Context) error {
|
||||
func (h helpValue) BeforeApply(ctx *Context) error {
|
||||
options := ctx.Kong.helpOptions
|
||||
options.Summary = false
|
||||
err := ctx.Kong.help(options, ctx)
|
||||
@@ -209,7 +210,13 @@ func (k *Kong) Parse(args []string) (ctx *Context, err error) {
|
||||
if ctx.Error != nil {
|
||||
return nil, &ParseError{error: ctx.Error, Context: ctx}
|
||||
}
|
||||
if err = k.applyHook(ctx, "BeforeHook"); err != nil {
|
||||
if err = k.applyHook(ctx, "BeforeResolve"); err != nil {
|
||||
return nil, &ParseError{error: err, Context: ctx}
|
||||
}
|
||||
if err = ctx.Resolve(); err != nil {
|
||||
return nil, &ParseError{error: err, Context: ctx}
|
||||
}
|
||||
if err = k.applyHook(ctx, "BeforeApply"); err != nil {
|
||||
return nil, &ParseError{error: err, Context: ctx}
|
||||
}
|
||||
if err = ctx.Validate(); err != nil {
|
||||
@@ -218,7 +225,7 @@ func (k *Kong) Parse(args []string) (ctx *Context, err error) {
|
||||
if _, err = ctx.Apply(); err != nil {
|
||||
return nil, &ParseError{error: err, Context: ctx}
|
||||
}
|
||||
if err = k.applyHook(ctx, "AfterHook"); err != nil {
|
||||
if err = k.applyHook(ctx, "AfterApply"); err != nil {
|
||||
return nil, &ParseError{error: err, Context: ctx}
|
||||
}
|
||||
return ctx, nil
|
||||
@@ -306,6 +313,20 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
|
||||
k.Exit(1)
|
||||
}
|
||||
|
||||
// LoadConfig from path using the loader configured via Configuration(loader).
|
||||
//
|
||||
// "path" will have ~/ expanded.
|
||||
func (k *Kong) LoadConfig(path string) (ResolverFunc, error) {
|
||||
path = expandPath(path)
|
||||
r, err := os.Open(path) // nolint: gas
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
return k.loader(r)
|
||||
}
|
||||
|
||||
func catch(err *error) {
|
||||
msg := recover()
|
||||
if test, ok := msg.(Error); ok {
|
||||
|
||||
+4
-4
@@ -362,12 +362,12 @@ type hookContext struct {
|
||||
|
||||
type hookValue string
|
||||
|
||||
func (h *hookValue) BeforeHook(ctx *hookContext) error {
|
||||
func (h *hookValue) BeforeApply(ctx *hookContext) error {
|
||||
ctx.values = append(ctx.values, "before:"+string(*h))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hookValue) AfterHook(ctx *hookContext) error {
|
||||
func (h *hookValue) AfterApply(ctx *hookContext) error {
|
||||
ctx.values = append(ctx.values, "after:"+string(*h))
|
||||
return nil
|
||||
}
|
||||
@@ -377,12 +377,12 @@ type hookCmd struct {
|
||||
Three hookValue
|
||||
}
|
||||
|
||||
func (h *hookCmd) BeforeHook(ctx *hookContext) error {
|
||||
func (h *hookCmd) BeforeApply(ctx *hookContext) error {
|
||||
ctx.cmd = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hookCmd) AfterHook(ctx *hookContext) error {
|
||||
func (h *hookCmd) AfterApply(ctx *hookContext) error {
|
||||
ctx.cmd = true
|
||||
return nil
|
||||
}
|
||||
|
||||
+5
-11
@@ -2,7 +2,6 @@ package kong
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -116,8 +115,8 @@ func Writers(stdout, stderr io.Writer) OptionFunc {
|
||||
//
|
||||
// There are two hook points:
|
||||
//
|
||||
// BeforeHook(...) error
|
||||
// AfterHook(...) error
|
||||
// BeforeApply(...) error
|
||||
// AfterApply(...) error
|
||||
//
|
||||
// Called before validation/assignment, and immediately after validation/assignment, respectively.
|
||||
func Bind(args ...interface{}) OptionFunc {
|
||||
@@ -179,17 +178,12 @@ type ConfigurationFunc func(r io.Reader) (ResolverFunc, error)
|
||||
// ~ expansion will occur on the provided paths.
|
||||
func Configuration(loader ConfigurationFunc, paths ...string) OptionFunc {
|
||||
return func(k *Kong) error {
|
||||
k.loader = loader
|
||||
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 {
|
||||
resolver, _ := k.LoadConfig(path)
|
||||
if resolver != nil {
|
||||
k.resolvers = append(k.resolvers, resolver)
|
||||
}
|
||||
_ = r.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package kong
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ConfigFlag uses the configured (via kong.Configuration(loader)) configuration loader to load configuration
|
||||
// from a file specified by a flag.
|
||||
//
|
||||
// Use this as a flag value to support loading of custom configuration via a flag.
|
||||
type ConfigFlag string
|
||||
|
||||
// BeforeResolve adds a resolver.
|
||||
func (c ConfigFlag) BeforeResolve(kong *Kong, ctx *Context, trace *Path) error {
|
||||
if kong.loader == nil {
|
||||
return fmt.Errorf("Kong must be configured with kong.Configuration(...)")
|
||||
}
|
||||
path := string(ctx.FlagValue(trace.Flag).(ConfigFlag))
|
||||
resolver, err := kong.LoadConfig(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.AddResolver(resolver)
|
||||
return nil
|
||||
}
|
||||
|
||||
// VersionFlag is a flag type that can be used to display a version number, stored in the "version" variable.
|
||||
type VersionFlag bool
|
||||
|
||||
// BeforeApply writes the version variable and terminates with a 0 exit status.
|
||||
func (v VersionFlag) BeforeApply(app *Kong, vars Vars) error {
|
||||
fmt.Fprintln(app.Stdout, vars["version"])
|
||||
app.Exit(0)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package kong
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfigFlag(t *testing.T) {
|
||||
var cli struct {
|
||||
Config ConfigFlag
|
||||
Flag string
|
||||
}
|
||||
|
||||
w, err := ioutil.TempFile("", "")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(w.Name())
|
||||
w.WriteString(`{"flag": "hello world"}`) // nolint: errcheck
|
||||
w.Close()
|
||||
|
||||
p := Must(&cli, Configuration(JSON))
|
||||
_, err = p.Parse([]string{"--config", w.Name()})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", cli.Flag)
|
||||
}
|
||||
|
||||
func TestVersionFlag(t *testing.T) {
|
||||
var cli struct {
|
||||
Version VersionFlag
|
||||
}
|
||||
w := &strings.Builder{}
|
||||
p := Must(&cli, Vars{"version": "0.1.1"})
|
||||
p.Stdout = w
|
||||
called := 1
|
||||
p.Exit = func(s int) { called = s }
|
||||
|
||||
_, err := p.Parse([]string{"--version"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "0.1.1", strings.TrimSpace(w.String()))
|
||||
require.Equal(t, 0, called)
|
||||
}
|
||||
Reference in New Issue
Block a user