diff --git a/README.md b/README.md index f69aec9..133665c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/_examples/docker/main.go b/_examples/docker/main.go index 0fd01f3..3c3afcb 100644 --- a/_examples/docker/main.go +++ b/_examples/docker/main.go @@ -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 diff --git a/context.go b/context.go index 2661e68..6df5c36 100644 --- a/context.go +++ b/context.go @@ -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 diff --git a/hooks.go b/hooks.go new file mode 100644 index 0000000..08fa171 --- /dev/null +++ b/hooks.go @@ -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 +} diff --git a/kong.go b/kong.go index fb0bbf6..fad3113 100644 --- a/kong.go +++ b/kong.go @@ -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 { diff --git a/kong_test.go b/kong_test.go index f937b21..f1e8e47 100644 --- a/kong_test.go +++ b/kong_test.go @@ -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 } diff --git a/options.go b/options.go index 1420f7e..f20833a 100644 --- a/options.go +++ b/options.go @@ -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 } diff --git a/util.go b/util.go new file mode 100644 index 0000000..addd64f --- /dev/null +++ b/util.go @@ -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 +} diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..358f43d --- /dev/null +++ b/util_test.go @@ -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) +}