Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: alecthomas/kong
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.9.0
Choose a base ref
...
head repository: alecthomas/kong
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.10.0
Choose a head ref
  • 3 commits
  • 12 files changed
  • 4 contributors

Commits on Mar 13, 2025

  1. feat: Placeholder string interpolation. (#510)

    Add support string interpolation in placeholder values.
    cwize1 authored Mar 13, 2025
    Copy the full SHA
    44be791 View commit details

Commits on Mar 16, 2025

  1. chore(deps): update all non-major dependencies (#506)

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
    renovate[bot] authored Mar 16, 2025
    Copy the full SHA
    1edf069 View commit details

Commits on Mar 22, 2025

  1. feat: Allow configuring global hooks via Kong's functional options (#511

    )
    
    Lets you pass `kong.WithBeforeApply` along with a function that supports dynamic bindings to register a `BeforeApply` hook without tying it directly to a node in the schema.
    
    Co-authored-by: Sutina Wipawiwat <swipawiwat@squareup.com>
    boblail and wsutina authored Mar 22, 2025
    Copy the full SHA
    78d4066 View commit details
Showing with 178 additions and 17 deletions.
  1. +16 −6 README.md
  2. +27 −5 _examples/server/go.mod
  3. +10 −0 _examples/server/go.sum
  4. 0 bin/{.go-1.24.0.pkg → .go-1.24.1.pkg}
  5. 0 bin/{.lefthook-1.10.10.pkg → .lefthook-1.11.3.pkg}
  6. +1 −1 bin/go
  7. +1 −1 bin/gofmt
  8. +1 −1 bin/lefthook
  9. +6 −0 hooks.go
  10. +19 −1 kong.go
  11. +63 −2 kong_test.go
  12. +34 −0 options.go
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -308,10 +308,16 @@ func main() {

## Hooks: BeforeReset(), BeforeResolve(), BeforeApply(), AfterApply()

If a node in the CLI, or any of its embedded fields, has a `BeforeReset(...) error`, `BeforeResolve
(...) error`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those
methods will be called before values are reset, before validation/assignment,
and after validation/assignment, respectively.
If a node in the CLI, or any of its embedded fields, implements a `BeforeReset(...) error`, `BeforeResolve
(...) error`, `BeforeApply(...) error` and/or `AfterApply(...) error` method, those will be called as Kong
resets, resolves, validates, and assigns values to the node.

| Hook | Description |
| --------------- | ----------------------------------------------------------------------------------------------------------- |
| `BeforeReset` | Invoked before values are reset to their defaults (as defined by the grammar) or to zero values |
| `BeforeResolve` | Invoked before resolvers are applied to a node |
| `BeforeApply` | Invoked before the traced command line arguments are applied to the grammar |
| `AfterApply` | Invoked after command line arguments are applied to the grammar **and validated**` |

The `--help` flag is implemented with a `BeforeReset` hook.

@@ -340,6 +346,10 @@ func main() {
}
```

It's also possible to register these hooks with the functional options
`kong.WithBeforeReset`, `kong.WithBeforeResolve`, `kong.WithBeforeApply`, and
`kong.WithAfterApply`.

## The Bind() option

Arguments to hooks are provided via the `Run(...)` method or `Bind(...)` option. `*Kong`, `*Context`, `*Path` and parent commands are also bound and finally, hooks can also contribute bindings via `kong.Context.Bind()` and `kong.Context.BindTo()`.
@@ -620,8 +630,8 @@ also supports dynamically adding commands via `kong.DynamicCommand()`.
## Variable interpolation
Kong supports limited variable interpolation into help strings, enum lists and
default values.
Kong supports limited variable interpolation into help strings, placeholder strings,
enum lists and default values.
Variables are in the form:
32 changes: 27 additions & 5 deletions _examples/server/go.mod
Original file line number Diff line number Diff line change
@@ -1,16 +1,38 @@
module kong_server

go 1.13
go 1.23.0

toolchain go1.24.1

require (
github.com/alecthomas/colour v0.1.0
github.com/alecthomas/kong v1.8.1
github.com/alecthomas/kong v1.9.0
github.com/chzyer/readline v1.5.1
github.com/chzyer/test v1.0.0 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gliderlabs/ssh v0.3.8
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/kr/pty v1.1.8
golang.org/x/crypto v0.36.0
)

require (
github.com/alecthomas/assert/v2 v2.11.0 // indirect
github.com/alecthomas/repr v0.4.0 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/chzyer/logex v1.2.1 // indirect
github.com/chzyer/test v1.0.0 // indirect
github.com/creack/pty v1.1.7 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
golang.org/x/crypto v0.33.0
github.com/yuin/goldmark v1.4.13 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect
)
10 changes: 10 additions & 0 deletions _examples/server/go.sum
Original file line number Diff line number Diff line change
@@ -26,6 +26,8 @@ github.com/alecthomas/kong v1.7.0 h1:MnT8+5JxFDCvISeI6vgd/mFbAJwueJ/pqQNzZMsiqZE
github.com/alecthomas/kong v1.7.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/kong v1.8.1 h1:6aamvWBE/REnR/BCq10EcozmcpUPc5aGI1lPAWdB0EE=
github.com/alecthomas/kong v1.8.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/kong v1.9.0 h1:Wgg0ll5Ys7xDnpgYBuBn/wPeLGAuK0NvYmEcisJgrIs=
github.com/alecthomas/kong v1.9.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU=
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
@@ -108,6 +110,8 @@ golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -134,6 +138,7 @@ golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -175,6 +180,8 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -210,6 +217,8 @@ golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -227,6 +236,7 @@ golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion bin/go
2 changes: 1 addition & 1 deletion bin/gofmt
2 changes: 1 addition & 1 deletion bin/lefthook
6 changes: 6 additions & 0 deletions hooks.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package kong

// BeforeReset is a documentation-only interface describing hooks that run before defaults values are applied.
type BeforeReset interface {
// This is not the correct signature - see README for details.
BeforeReset(args ...any) error
}

// BeforeResolve is a documentation-only interface describing hooks that run before resolvers are applied.
type BeforeResolve interface {
// This is not the correct signature - see README for details.
20 changes: 19 additions & 1 deletion kong.go
Original file line number Diff line number Diff line change
@@ -71,6 +71,8 @@ type Kong struct {
postBuildOptions []Option
embedded []embedded
dynamicCommands []*dynamicCommand

hooks map[string][]reflect.Value
}

// New creates a new Kong parser on grammar.
@@ -84,6 +86,7 @@ func New(grammar any, options ...Option) (*Kong, error) {
registry: NewRegistry().RegisterDefaults(),
vars: Vars{},
bindings: bindings{},
hooks: make(map[string][]reflect.Value),
helpFormatter: DefaultHelpValueFormatter,
ignoreFields: make([]*regexp.Regexp, 0),
flagNamer: func(s string) string {
@@ -270,6 +273,11 @@ func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) {
if len(value.Flag.Envs) != 0 {
updatedVars["env"] = value.Flag.Envs[0]
}

value.Flag.PlaceHolder, err = interpolate(value.Flag.PlaceHolder, vars, updatedVars)
if err != nil {
return fmt.Errorf("placeholder value for %s: %s", value.Summary(), err)
}
}
value.Help, err = interpolate(value.Help, vars, updatedVars)
if err != nil {
@@ -361,7 +369,7 @@ func (k *Kong) applyHook(ctx *Context, name string) error {
default:
panic("unsupported Path")
}
for _, method := range getMethods(value, name) {
for _, method := range k.getMethods(value, name) {
binds := k.bindings.clone()
binds.add(ctx, trace)
binds.add(trace.Node().Vars().CloneWith(k.vars))
@@ -375,6 +383,16 @@ func (k *Kong) applyHook(ctx *Context, name string) error {
return k.applyHookToDefaultFlags(ctx, ctx.Path[0].Node(), name)
}

func (k *Kong) getMethods(value reflect.Value, name string) []reflect.Value {
return append(
// Identify callbacks by reflecting on value
getMethods(value, name),

// Identify callbacks that were registered with a kong.Option
k.hooks[name]...,
)
}

// Call hook on any unset flags with default values.
func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error {
if node == nil {
65 changes: 63 additions & 2 deletions kong_test.go
Original file line number Diff line number Diff line change
@@ -588,6 +588,65 @@ func TestHooks(t *testing.T) {
}
}

func TestGlobalHooks(t *testing.T) {
var cli struct {
One struct {
Two string `kong:"arg,optional"`
Three string
} `cmd:""`
}

called := []string{}
log := func(name string) any {
return func(value *kong.Path) error {
switch {
case value.App != nil:
called = append(called, fmt.Sprintf("%s (app)", name))

case value.Positional != nil:
called = append(called, fmt.Sprintf("%s (arg) %s", name, value.Positional.Name))

case value.Flag != nil:
called = append(called, fmt.Sprintf("%s (flag) %s", name, value.Flag.Name))

case value.Argument != nil:
called = append(called, fmt.Sprintf("%s (arg) %s", name, value.Argument.Name))

case value.Command != nil:
called = append(called, fmt.Sprintf("%s (cmd) %s", name, value.Command.Name))
}
return nil
}
}
p := mustNew(t, &cli,
kong.WithBeforeReset(log("BeforeReset")),
kong.WithBeforeResolve(log("BeforeResolve")),
kong.WithBeforeApply(log("BeforeApply")),
kong.WithAfterApply(log("AfterApply")),
)

_, err := p.Parse([]string{"one", "two", "--three=THREE"})
assert.NoError(t, err)
assert.Equal(t, []string{
"BeforeReset (app)",
"BeforeReset (cmd) one",
"BeforeReset (arg) two",
"BeforeReset (flag) three",
"BeforeResolve (app)",
"BeforeResolve (cmd) one",
"BeforeResolve (arg) two",
"BeforeResolve (flag) three",
"BeforeApply (app)",
"BeforeApply (cmd) one",
"BeforeApply (arg) two",
"BeforeApply (flag) three",
"AfterApply (app)",
"AfterApply (cmd) one",
"AfterApply (arg) two",
"AfterApply (flag) three",
}, called)
}

func TestShort(t *testing.T) {
var cli struct {
Bool bool `short:"b"`
@@ -766,8 +825,8 @@ func TestPassesThroughOriginalCommandError(t *testing.T) {

func TestInterpolationIntoModel(t *testing.T) {
var cli struct {
Flag string `default:"${default_value}" help:"Help, I need ${somebody}" enum:"${enum}"`
EnumRef string `enum:"a,b" required:"" help:"One of ${enum}"`
Flag string `default:"${default_value}" help:"Help, I need ${somebody}" enum:"${enum}" placeholder:"${enum}"`
EnumRef string `enum:"a,b" required:"" help:"One of ${enum}" placeholder:"${enum}"`
EnvRef string `env:"${env}" help:"God ${env}"`
}
_, err := kong.New(&cli)
@@ -787,7 +846,9 @@ func TestInterpolationIntoModel(t *testing.T) {
assert.Equal(t, "Help, I need chickens!", flag.Help)
assert.Equal(t, map[string]bool{"a": true, "b": true, "c": true, "d": true}, flag.EnumMap())
assert.Equal(t, []string{"a", "b", "c", "d"}, flag.EnumSlice())
assert.Equal(t, "a,b,c,d", flag.PlaceHolder)
assert.Equal(t, "One of a,b", flag2.Help)
assert.Equal(t, "a,b", flag2.PlaceHolder)
assert.Equal(t, []string{"SAVE_THE_QUEEN"}, flag3.Envs)
assert.Equal(t, "God SAVE_THE_QUEEN", flag3.Help)
}
34 changes: 34 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
@@ -123,6 +123,40 @@ func PostBuild(fn func(*Kong) error) Option {
})
}

// WithBeforeReset registers a hook to run before fields values are reset to their defaults
// (as specified in the grammar) or to zero values.
func WithBeforeReset(fn any) Option {
return withHook("BeforeReset", fn)
}

// WithBeforeResolve registers a hook to run before resolvers are applied.
func WithBeforeResolve(fn any) Option {
return withHook("BeforeResolve", fn)
}

// WithBeforeApply registers a hook to run before command line arguments are applied to the grammar.
func WithBeforeApply(fn any) Option {
return withHook("BeforeApply", fn)
}

// WithAfterApply registers a hook to run after values are applied to the grammar and validated.
func WithAfterApply(fn any) Option {
return withHook("AfterApply", fn)
}

// withHook registers a named hook.
func withHook(name string, fn any) Option {
value := reflect.ValueOf(fn)
if value.Kind() != reflect.Func {
panic(fmt.Errorf("expected function, got %s", value.Type()))
}

return OptionFunc(func(k *Kong) error {
k.hooks[name] = append(k.hooks[name], value)
return nil
})
}

// Name overrides the application name.
func Name(name string) Option {
return PostBuild(func(k *Kong) error {