Repository: alecthomas/kong
Pull request #360 (2023-04-22 13:50:26)

Support string pointers for path mapper
I have a use case where I would like to differentiate between an unset path and an empty string path (which expands to the current directory).

This PR updates the `pathMapper` function to allow a `path` type to be applied to a `*string` to make this differentiation per the pointer behaviour described here: https://github.com/alecthomas/kong#pointers


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 334898c..3396e46 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,24 +4,55 @@ on:
       - master
   pull_request:
 name: CI
+
 jobs:
   test:
-    name: Test
+    name: Test / Go ${{ matrix.go }}
     runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        # These are the release channels.
+        # Hermit will handle installing the right patch.
+        go: ["1.20", "1.21"]
     steps:
       - name: Checkout code
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
       - name: Init Hermit
-        run: ./bin/hermit env -r >> $GITHUB_ENV
+        run: ./bin/hermit env -r >> "$GITHUB_ENV"
+      - name: Install Go ${{ matrix.go }}
+        run: |
+          hermit install go@"${GO_VERSION}"
+          go version
+        env:
+          GO_VERSION: ${{ matrix.go }}
       - name: Test
         run: go test ./...
+
+  test-windows:
+    name: Test / Windows / Go ${{ matrix.go }}
+    runs-on: windows-latest
+    strategy:
+      matrix:
+        # These are versions for GitHub's setup-go.
+        # '.x' will pick the latest patch release.
+        go: ["1.20.x", "1.21.x"]
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+      - name: Setup Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: ${{ matrix.go }}
+      - name: Test
+        run: go test ./...
+
   lint:
     name: Lint
     runs-on: ubuntu-latest
     steps:
       - name: Checkout code
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
       - name: Init Hermit
-        run: ./bin/hermit env -r >> $GITHUB_ENV
+        run: ./bin/hermit env -r >> "$GITHUB_ENV"
       - name: golangci-lint
         run: golangci-lint run
diff --git a/.gitignore b/.gitignore
index ba077a4..e69de29 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,1 +0,0 @@
-bin
diff --git a/.golangci.yml b/.golangci.yml
index 1ee8fa6..e8980bf 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -41,10 +41,21 @@ linters:
     - exhaustruct
     - nonamedreturns
     - nilnil
+    - nosnakecase # deprecated since v1.48.1
+    - structcheck # deprecated since v1.49.0
+    - deadcode    # deprecated since v1.49.0
+    - varcheck    # deprecated since v1.49.0
+    - depguard    # nothing to guard against yet
+    - tagalign    # hurts readability of kong tags
 
 linters-settings:
   govet:
     check-shadowing: true
+    # These govet checks are disabled by default, but they're useful.
+    enable:
+      - niliness
+      - sortslice
+      - unusedwrite
   dupl:
     threshold: 100
   gocyclo:
@@ -65,4 +76,14 @@ issues:
     - 'bad syntax for struct tag key'
     - 'bad syntax for struct tag pair'
     - 'result .* \(error\) is always nil'
-    - 'package io/ioutil is deprecated'
+
+  exclude-rules:
+    # Don't warn on unused parameters.
+    # Parameter names are useful for documentation.
+    # Replacing them with '_' hides useful information.
+    - linters: [revive]
+      text: 'unused-parameter: parameter \S+ seems to be unused, consider removing or renaming it as _'
+
+    # Duplicate words are okay in tests.
+    - linters: [dupword]
+      path: _test\.go
diff --git a/README.md b/README.md
index 35b07c6..22e00f0 100644
--- a/README.md
+++ b/README.md
@@ -2,47 +2,45 @@
 <p align="center"><img width="90%" src="kong.png" /></p>
 
 # Kong is a command-line parser for Go
+
 [![](https://godoc.org/github.com/alecthomas/kong?status.svg)](http://godoc.org/github.com/alecthomas/kong) [![CircleCI](https://img.shields.io/circleci/project/github/alecthomas/kong.svg)](https://circleci.com/gh/alecthomas/kong) [![Go Report Card](https://goreportcard.com/badge/github.com/alecthomas/kong)](https://goreportcard.com/report/github.com/alecthomas/kong) [![Slack chat](https://img.shields.io/static/v1?logo=slack&style=flat&label=slack&color=green&message=gophers)](https://gophers.slack.com/messages/CN9DS8YF3)
 
-<!-- https://github.com/naokazuterada/MarkdownTOC -->
-
-<!-- MarkdownTOC autolink="true" style="ordered" indent="    " -->
-
-1. [Introduction](#introduction)
-1. [Help](#help)
-    1. [Help as a user of a Kong application](#help-as-a-user-of-a-kong-application)
-    1. [Defining help in Kong](#defining-help-in-kong)
-        1. [Showing the _command_'s detailed help](#showing-the-_command_s-detailed-help)
-        1. [Showing an _argument_'s detailed help](#showing-an-_argument_s-detailed-help)
-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. [Hooks: BeforeReset\(\), BeforeResolve\(\), BeforeApply\(\), AfterApply\(\) and the Bind\(\) option](#hooks-beforereset-beforeresolve-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)
-1. [Positional arguments](#positional-arguments)
-1. [Slices](#slices)
-1. [Maps](#maps)
-1. [Nested data structure](#nested-data-structure)
-1. [Custom named decoders](#custom-named-decoders)
-1. [Supported field types](#supported-field-types)
-1. [Custom decoders \(mappers\)](#custom-decoders-mappers)
-1. [Supported tags](#supported-tags)
-1. [Plugins](#plugins)
-1. [Dynamic Commands](#dynamic-commands)
-1. [Variable interpolation](#variable-interpolation)
-1. [Validation](#validation)
-1. [Modifying Kong's behaviour](#modifying-kongs-behaviour)
-    1. [`Name(help)` and `Description(help)` - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description)
-    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. [`ConfigureHelp(HelpOptions)` and `Help(HelpFunc)` - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help)
-    1. [`Bind(...)` - bind values for callback hooks and Run\(\) methods](#bind---bind-values-for-callback-hooks-and-run-methods)
-    1. [Other options](#other-options)
-
-<!-- /MarkdownTOC -->
+<!-- TOC depthfrom:2 depthto:3 -->
+
+- [Introduction](#introduction)
+- [Help](#help)
+  - [Help as a user of a Kong application](#help-as-a-user-of-a-kong-application)
+  - [Defining help in Kong](#defining-help-in-kong)
+- [Command handling](#command-handling)
+  - [Switch on the command string](#switch-on-the-command-string)
+  - [Attach a Run... error method to each command](#attach-a-run-error-method-to-each-command)
+- [Hooks: BeforeReset, BeforeResolve, BeforeApply, AfterApply and the Bind option](#hooks-beforereset-beforeresolve-beforeapply-afterapply-and-the-bind-option)
+- [Flags](#flags)
+- [Commands and sub-commands](#commands-and-sub-commands)
+- [Branching positional arguments](#branching-positional-arguments)
+- [Positional arguments](#positional-arguments)
+- [Slices](#slices)
+- [Maps](#maps)
+- [Pointers](#pointers)
+- [Nested data structure](#nested-data-structure)
+- [Custom named decoders](#custom-named-decoders)
+- [Supported field types](#supported-field-types)
+- [Custom decoders mappers](#custom-decoders-mappers)
+- [Supported tags](#supported-tags)
+- [Plugins](#plugins)
+- [Dynamic Commands](#dynamic-commands)
+- [Variable interpolation](#variable-interpolation)
+- [Validation](#validation)
+- [Modifying Kong's behaviour](#modifying-kongs-behaviour)
+  - [Namehelp and Descriptionhelp - set the application name description](#namehelp-and-descriptionhelp---set-the-application-name-description)
+  - [Configurationloader, paths... - load defaults from configuration files](#configurationloader-paths---load-defaults-from-configuration-files)
+  - [Resolver... - support for default values from external sources](#resolver---support-for-default-values-from-external-sources)
+  - [\*Mapper... - customising how the command-line is mapped to Go values](#mapper---customising-how-the-command-line-is-mapped-to-go-values)
+  - [ConfigureHelpHelpOptions and HelpHelpFunc - customising help](#configurehelphelpoptions-and-helphelpfunc---customising-help)
+  - [Bind... - bind values for callback hooks and Run methods](#bind---bind-values-for-callback-hooks-and-run-methods)
+  - [Other options](#other-options)
+
+<!-- /TOC -->
 
 ## Introduction
 
@@ -137,13 +135,14 @@ also be interpolated into the help string.
 Finally, any command, or argument type implementing the interface
 `Help() string` will have this function called to retrieve more detail to
 augment the help tag. This allows for much more descriptive text than can
-fit in Go tags.  [See _examples/shell/help](./_examples/shell/help)
+fit in Go tags. [See \_examples/shell/help](./_examples/shell/help)
 
 #### Showing the _command_'s detailed help
 
 A command's additional help text is _not_ shown from top-level help, but can be displayed within contextual help:
 
 **Top level help**
+
 ```bash
  $ go run ./_examples/shell/help --help
 Usage: help <command>
@@ -159,6 +158,7 @@ Commands:
 ```
 
 **Contextual**
+
 ```bash
  $ go run ./_examples/shell/help echo --help
 Usage: help echo <msg>
@@ -180,6 +180,7 @@ Flags:
 Custom help will only be shown for _positional arguments with named fields_ ([see the README section on positional arguments for more details on what that means](../../../README.md#branching-positional-arguments))
 
 **Contextual argument help**
+
 ```bash
  $ go run ./_examples/shell/help msg --help
 Usage: help echo <msg>
@@ -329,7 +330,7 @@ var cli struct {
 
 func main() {
   // Debug logger going to discard.
-  logger := log.New(ioutil.Discard, "", log.LstdFlags)
+  logger := log.New(io.Discard, "", log.LstdFlags)
 
   ctx := kong.Parse(&cli, kong.Bind(logger))
 
@@ -337,9 +338,43 @@ func main() {
 }
 ```
 
+Another example of using hooks is load the env-file:
+
+```go
+package main
+
+import (
+  "fmt"
+  "github.com/alecthomas/kong"
+  "github.com/joho/godotenv"
+)
+
+type EnvFlag string
+
+// BeforeResolve loads env file.
+func (c EnvFlag) BeforeReset(ctx *kong.Context, trace *kong.Path) error {
+  path := string(ctx.FlagValue(trace.Flag).(EnvFlag)) // nolint
+  path = kong.ExpandPath(path)
+  if err := godotenv.Load(path); err != nil {
+    return err
+  }
+  return nil
+}
+
+var CLI struct {
+  EnvFile EnvFlag
+  Flag `env:"FLAG"`
+}
+
+func main() {
+  _ = kong.Parse(&CLI)
+  fmt.Println(CLI.Flag)
+}
+```
+
 ## Flags
 
-Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure *not* tagged with `cmd` or `arg` will be a flag. Flags are optional by default.
+Any [mapped](#mapper---customising-how-the-command-line-is-mapped-to-go-values) field in the command structure _not_ tagged with `cmd` or `arg` will be a flag. Flags are optional by default.
 
 eg. The command-line `app [--flag="foo"]` can be represented by the following.
 
@@ -479,25 +514,22 @@ Kong includes a number of builtin custom type mappers. These can be used by
 specifying the tag `type:"<type>"`. They are registered with the option
 function `NamedMapper(name, mapper)`.
 
-| Name           | Description                                                                                        |
-| -------------- | -------------------------------------------------------------------------------------------------- |
-| `path`         | A path. ~ expansion is applied. `-` is accepted for stdout, and will be passed unaltered.          |
-| `existingfile` | An existing file. ~ expansion is applied. `-` is accepted for stdin, and will be passed unaltered. |
-| `existingdir`  | An existing directory. ~ expansion is applied.                                                     |
-| `counter`      | Increment a numeric field. Useful for `-vvv`. Can accept `-s`, `--long` or `--long=N`.             |
+| Name           | Description                                                                                                            |
+| -------------- | ---------------------------------------------------------------------------------------------------------------------- |
+| `path`         | A path. ~ expansion is applied. `-` is accepted for stdout, and will be passed unaltered.                              |
+| `existingfile` | An existing file. ~ expansion is applied. `-` is accepted for stdin, and will be passed unaltered.                     |
+| `existingdir`  | An existing directory. ~ expansion is applied.                                                                         |
+| `counter`      | Increment a numeric field. Useful for `-vvv`. Can accept `-s`, `--long` or `--long=N`.                                 |
 | `filecontent`  | Read the file at path into the field. ~ expansion is applied. `-` is accepted for stdin, and will be passed unaltered. |
 
-
 Slices and maps treat type tags specially. For slices, the `type:""` tag
 specifies the element type. For maps, the tag has the format
 `tag:"[<key>]:[<value>]"` where either may be omitted.
 
 ## Supported field types
 
-
 ## Custom decoders (mappers)
 
-
 Any field implementing `encoding.TextUnmarshaler` or `json.Unmarshaler` will use those interfaces
 for decoding values. Kong also includes builtin support for many common Go types:
 
@@ -522,7 +554,7 @@ Tags can be in two forms:
 Both can coexist with standard Tag parsing.
 
 | Tag                  | Description                                                                                                                                                                                                                                                                                                                    |
-|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
 | `cmd:""`             | If present, struct is a command.                                                                                                                                                                                                                                                                                               |
 | `arg:""`             | If present, field is an argument. Required by default.                                                                                                                                                                                                                                                                         |
 | `env:"X,Y,..."`      | Specify envars to use for default value. The envs are resolved in the declared order. The first value found is used.                                                                                                                                                                                                           |
@@ -534,7 +566,7 @@ Both can coexist with standard Tag parsing.
 | `default:"1"`        | On a command, make it the default.                                                                                                                                                                                                                                                                                             |
 | `default:"withargs"` | On a command, make it the default and allow args/flags from that command                                                                                                                                                                                                                                                       |
 | `short:"X"`          | Short name, if flag.                                                                                                                                                                                                                                                                                                           |
-| `aliases:"X,Y"`      | One or more aliases (for cmd).                                                                                                                                                                                                                                                                                                 |
+| `aliases:"X,Y"`      | One or more aliases (for cmd or flag).                                                                                                                                                                                                                                                                                                 |
 | `required:""`        | If present, flag/arg is required.                                                                                                                                                                                                                                                                                              |
 | `optional:""`        | If present, flag/arg is optional.                                                                                                                                                                                                                                                                                              |
 | `hidden:""`          | If present, command or flag is hidden.                                                                                                                                                                                                                                                                                         |
@@ -629,7 +661,7 @@ interface:
 type Validatable interface {
     Validate() error
  }
- ```
+```
 
 If one of these nodes is in the active command-line it will be called during
 normal validation.
@@ -660,6 +692,13 @@ kong.Parse(&cli, kong.Configuration(kong.JSON, "/etc/myapp.json", "~/.myapp.json
 
 [See the tests](https://github.com/alecthomas/kong/blob/master/resolver_test.go#L206) for an example of how the JSON file is structured.
 
+#### List of Configuration Loaders
+
+- [YAML](https://github.com/alecthomas/kong-yaml)
+- [HCL](https://github.com/alecthomas/kong-hcl)
+- [TOML](https://github.com/alecthomas/kong-toml)
+- [JSON](https://github.com/alecthomas/kong)
+
 ### `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.
@@ -704,4 +743,4 @@ See the [section on hooks](#hooks-beforeresolve-beforeapply-afterapply-and-the-b
 
 ### Other options
 
-The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option).
\ No newline at end of file
+The full set of options can be found [here](https://godoc.org/github.com/alecthomas/kong#Option).
diff --git a/_examples/server/go.mod b/_examples/server/go.mod
index a5147ca..093e0fe 100644
--- a/_examples/server/go.mod
+++ b/_examples/server/go.mod
@@ -4,14 +4,13 @@ go 1.13
 
 require (
 	github.com/alecthomas/colour v0.1.0
-	github.com/alecthomas/kong v0.2.1
-	github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
-	github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
+	github.com/alecthomas/kong v0.8.1
+	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.2.2
+	github.com/gliderlabs/ssh v0.3.6
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
 	github.com/kr/pty v1.1.8
 	github.com/mattn/go-isatty v0.0.12 // indirect
-	golang.org/x/crypto v0.1.0
+	golang.org/x/crypto v0.21.0
 )
diff --git a/_examples/server/go.sum b/_examples/server/go.sum
index 3693198..ee9204f 100644
--- a/_examples/server/go.sum
+++ b/_examples/server/go.sum
@@ -1,13 +1,21 @@
+github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
 github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk=
 github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
 github.com/alecthomas/kong v0.2.1 h1:V1tLBhyQBC4rsbXbcOvm3GBaytJSwRNX69fp1WJxbqQ=
 github.com/alecthomas/kong v0.2.1/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
+github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY=
+github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U=
+github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
+github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
 github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
 github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
+github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
 github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
 github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
 github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
@@ -18,8 +26,13 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjr
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
 github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
+github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
+github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8=
+github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
 github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
 github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
@@ -34,32 +47,77 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
+golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
+golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
 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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
+golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
+golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 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=
+golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
+golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
+golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
 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=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
 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=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/bin/.go-1.19.1.pkg b/bin/.go-1.22.0.pkg
similarity index 100%
rename from bin/.go-1.19.1.pkg
rename to bin/.go-1.22.0.pkg
diff --git a/bin/.golangci-lint-1.46.2.pkg b/bin/.golangci-lint-1.55.2.pkg
similarity index 100%
rename from bin/.golangci-lint-1.46.2.pkg
rename to bin/.golangci-lint-1.55.2.pkg
diff --git a/bin/go b/bin/go
index deef954..9286b4b 120000
--- a/bin/go
+++ b/bin/go
@@ -1,1 +1,1 @@
-.go-1.19.1.pkg
\ No newline at end of file
+.go-1.22.0.pkg
\ No newline at end of file
diff --git a/bin/gofmt b/bin/gofmt
index deef954..9286b4b 120000
--- a/bin/gofmt
+++ b/bin/gofmt
@@ -1,1 +1,1 @@
-.go-1.19.1.pkg
\ No newline at end of file
+.go-1.22.0.pkg
\ No newline at end of file
diff --git a/bin/golangci-lint b/bin/golangci-lint
index de42e34..5b19a9d 120000
--- a/bin/golangci-lint
+++ b/bin/golangci-lint
@@ -1,1 +1,1 @@
-.golangci-lint-1.46.2.pkg
\ No newline at end of file
+.golangci-lint-1.55.2.pkg
\ No newline at end of file
diff --git a/build.go b/build.go
index e23c115..a52d90f 100644
--- a/build.go
+++ b/build.go
@@ -296,6 +296,13 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
 			return failField(v, ft, "duplicate flag --%s", value.Name)
 		}
 		seenFlags["--"+value.Name] = true
+		for _, alias := range tag.Aliases {
+			aliasFlag := "--" + alias
+			if seenFlags[aliasFlag] {
+				return failField(v, ft, "duplicate flag %s", aliasFlag)
+			}
+			seenFlags[aliasFlag] = true
+		}
 		if tag.Short != 0 {
 			if seenFlags["-"+string(tag.Short)] {
 				return failField(v, ft, "duplicate short flag -%c", tag.Short)
@@ -304,6 +311,7 @@ func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv
 		}
 		flag := &Flag{
 			Value:       value,
+			Aliases:     tag.Aliases,
 			Short:       tag.Short,
 			PlaceHolder: tag.PlaceHolder,
 			Envs:        tag.Envs,
diff --git a/callbacks.go b/callbacks.go
index 3a8a45f..45ef0d2 100644
--- a/callbacks.go
+++ b/callbacks.go
@@ -41,7 +41,7 @@ func (b bindings) addProvider(provider interface{}) error {
 		errv := out[1]
 		var err error
 		if !errv.IsNil() {
-			err = errv.Interface().(error) // nolint
+			err = errv.Interface().(error) //nolint
 		}
 		return out[0], err
 	}
@@ -99,7 +99,7 @@ func callFunction(f reflect.Value, bindings bindings) error {
 	if out[0].IsNil() {
 		return nil
 	}
-	return out[0].Interface().(error) // nolint
+	return out[0].Interface().(error) //nolint
 }
 
 func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error) {
@@ -127,11 +127,3 @@ func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error)
 	}
 	return out, nil
 }
-
-func callMethod(name string, v, f reflect.Value, bindings bindings) error {
-	err := callFunction(f, bindings)
-	if err != nil {
-		return fmt.Errorf("%s.%s(): %w", v.Type(), name, err)
-	}
-	return nil
-}
diff --git a/camelcase.go b/camelcase.go
index acf29f7..a955b16 100644
--- a/camelcase.go
+++ b/camelcase.go
@@ -13,37 +13,37 @@ import (
 //
 // Examples
 //
-//   "" =>                     [""]
-//   "lowercase" =>            ["lowercase"]
-//   "Class" =>                ["Class"]
-//   "MyClass" =>              ["My", "Class"]
-//   "MyC" =>                  ["My", "C"]
-//   "HTML" =>                 ["HTML"]
-//   "PDFLoader" =>            ["PDF", "Loader"]
-//   "AString" =>              ["A", "String"]
-//   "SimpleXMLParser" =>      ["Simple", "XML", "Parser"]
-//   "vimRPCPlugin" =>         ["vim", "RPC", "Plugin"]
-//   "GL11Version" =>          ["GL", "11", "Version"]
-//   "99Bottles" =>            ["99", "Bottles"]
-//   "May5" =>                 ["May", "5"]
-//   "BFG9000" =>              ["BFG", "9000"]
-//   "BöseÜberraschung" =>     ["Böse", "Überraschung"]
-//   "Two  spaces" =>          ["Two", "  ", "spaces"]
-//   "BadUTF8\xe2\xe2\xa1" =>  ["BadUTF8\xe2\xe2\xa1"]
+//	"" =>                     [""]
+//	"lowercase" =>            ["lowercase"]
+//	"Class" =>                ["Class"]
+//	"MyClass" =>              ["My", "Class"]
+//	"MyC" =>                  ["My", "C"]
+//	"HTML" =>                 ["HTML"]
+//	"PDFLoader" =>            ["PDF", "Loader"]
+//	"AString" =>              ["A", "String"]
+//	"SimpleXMLParser" =>      ["Simple", "XML", "Parser"]
+//	"vimRPCPlugin" =>         ["vim", "RPC", "Plugin"]
+//	"GL11Version" =>          ["GL", "11", "Version"]
+//	"99Bottles" =>            ["99", "Bottles"]
+//	"May5" =>                 ["May", "5"]
+//	"BFG9000" =>              ["BFG", "9000"]
+//	"BöseÜberraschung" =>     ["Böse", "Überraschung"]
+//	"Two  spaces" =>          ["Two", "  ", "spaces"]
+//	"BadUTF8\xe2\xe2\xa1" =>  ["BadUTF8\xe2\xe2\xa1"]
 //
 // Splitting rules
 //
-//  1) If string is not valid UTF-8, return it without splitting as
+//  1. If string is not valid UTF-8, return it without splitting as
 //     single item array.
-//  2) Assign all unicode characters into one of 4 sets: lower case
+//  2. Assign all unicode characters into one of 4 sets: lower case
 //     letters, upper case letters, numbers, and all other characters.
-//  3) Iterate through characters of string, introducing splits
+//  3. Iterate through characters of string, introducing splits
 //     between adjacent characters that belong to different sets.
-//  4) Iterate through array of split strings, and if a given string
+//  4. Iterate through array of split strings, and if a given string
 //     is upper case:
-//       if subsequent string is lower case:
-//         move last character of upper case string to beginning of
-//         lower case string
+//     if subsequent string is lower case:
+//     move last character of upper case string to beginning of
+//     lower case string
 func camelCase(src string) (entries []string) {
 	// don't split invalid utf8
 	if !utf8.ValidString(src) {
diff --git a/context.go b/context.go
index 0ec10f4..b2bfea6 100644
--- a/context.go
+++ b/context.go
@@ -161,7 +161,7 @@ func (c *Context) Empty() bool {
 }
 
 // Validate the current context.
-func (c *Context) Validate() error { // nolint: gocyclo
+func (c *Context) Validate() error { //nolint: gocyclo
 	err := Visit(c.Model, func(node Visitable, next Next) error {
 		switch node := node.(type) {
 		case *Value:
@@ -201,7 +201,7 @@ func (c *Context) Validate() error { // nolint: gocyclo
 
 		case *Application:
 			value = node.Target
-			desc = node.Name
+			desc = ""
 
 		case *Node:
 			value = node.Target
@@ -209,7 +209,10 @@ func (c *Context) Validate() error { // nolint: gocyclo
 		}
 		if validate := isValidatable(value); validate != nil {
 			if err := validate.Validate(); err != nil {
-				return fmt.Errorf("%s: %w", desc, err)
+				if desc != "" {
+					return fmt.Errorf("%s: %w", desc, err)
+				}
+				return err
 			}
 		}
 	}
@@ -344,7 +347,7 @@ func (c *Context) endParsing() {
 	}
 }
 
-func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
+func (c *Context) trace(node *Node) (err error) { //nolint: gocyclo
 	positional := 0
 	node.Active = true
 
@@ -374,7 +377,7 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
 				switch {
 				case v == "-":
 					fallthrough
-				default: // nolint
+				default: //nolint
 					c.scan.Pop()
 					c.scan.PushTyped(token.Value, PositionalArgumentToken)
 
@@ -681,15 +684,24 @@ func flipBoolValue(value reflect.Value) error {
 
 func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
 	candidates := []string{}
+
 	for _, flag := range flags {
 		long := "--" + flag.Name
-		short := "-" + string(flag.Short)
-		neg := "--no-" + flag.Name
+		matched := long == match
 		candidates = append(candidates, long)
 		if flag.Short != 0 {
+			short := "-" + string(flag.Short)
+			matched = matched || (short == match)
 			candidates = append(candidates, short)
 		}
-		if short != match && long != match && !(match == neg && flag.Tag.Negatable) {
+		for _, alias := range flag.Aliases {
+			alias = "--" + alias
+			matched = matched || (alias == match)
+			candidates = append(candidates, alias)
+		}
+
+		neg := "--no-" + flag.Name
+		if !matched && !(match == neg && flag.Tag.Negatable) {
 			continue
 		}
 		// Found a matching flag.
@@ -759,7 +771,7 @@ func (c *Context) RunNode(node *Node, binds ...interface{}) (err error) {
 	}
 
 	for _, method := range methods {
-		if err = callMethod("Run", method.node.Target, method.method, method.binds); err != nil {
+		if err = callFunction(method.method, method.binds); err != nil {
 			return err
 		}
 	}
diff --git a/doc.go b/doc.go
index 78c4d11..7e53da7 100644
--- a/doc.go
+++ b/doc.go
@@ -2,31 +2,31 @@
 //
 // Here's an example:
 //
-//     shell rm [-f] [-r] <paths> ...
-//     shell ls [<paths> ...]
+//	shell rm [-f] [-r] <paths> ...
+//	shell ls [<paths> ...]
 //
 // This can be represented by the following command-line structure:
 //
-//     package main
+//	package main
 //
-//     import "github.com/alecthomas/kong"
+//	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."`
+//	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."`
+//	    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."`
-//     }
+//	  Ls struct {
+//	    Paths []string `arg optional help:"Paths to list." type:"path"`
+//	  } `cmd help:"List paths."`
+//	}
 //
-//     func main() {
-//       kong.Parse(&CLI)
-//     }
+//	func main() {
+//	  kong.Parse(&CLI)
+//	}
 //
 // See https://github.com/alecthomas/kong for details.
 package kong
diff --git a/go.mod b/go.mod
index 159f5f7..aee9b0d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,8 @@
 module github.com/alecthomas/kong
 
 require (
-	github.com/alecthomas/assert/v2 v2.1.0
-	github.com/alecthomas/repr v0.1.0
+	github.com/alecthomas/assert/v2 v2.6.0
+	github.com/alecthomas/repr v0.4.0
 )
 
 require github.com/hexops/gotextdiff v1.0.3 // indirect
diff --git a/go.sum b/go.sum
index f5d4fd4..ec451a0 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,12 @@
-github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0=
-github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
-github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
-github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
+github.com/alecthomas/assert/v2 v2.4.1 h1:mwPZod/d35nlaCppr6sFP0rbCL05WH9fIo7lvsf47zo=
+github.com/alecthomas/assert/v2 v2.4.1/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM=
+github.com/alecthomas/assert/v2 v2.5.0 h1:OJKYg53BQx06/bMRBSPDCO49CbCDNiUQXwdoNrt6x5w=
+github.com/alecthomas/assert/v2 v2.5.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM=
+github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
+github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8=
+github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
diff --git a/guesswidth.go b/guesswidth.go
index 46768e6..dfdc3f5 100644
--- a/guesswidth.go
+++ b/guesswidth.go
@@ -1,3 +1,4 @@
+//go:build appengine || (!linux && !freebsd && !darwin && !dragonfly && !netbsd && !openbsd)
 // +build appengine !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd
 
 package kong
diff --git a/guesswidth_unix.go b/guesswidth_unix.go
index db52595..0170055 100644
--- a/guesswidth_unix.go
+++ b/guesswidth_unix.go
@@ -27,9 +27,9 @@ func guessWidth(w io.Writer) int {
 
 		if _, _, err := syscall.Syscall6(
 			syscall.SYS_IOCTL,
-			uintptr(fd), // nolint: unconvert
+			uintptr(fd), //nolint: unconvert
 			uintptr(syscall.TIOCGWINSZ),
-			uintptr(unsafe.Pointer(&dimensions)), // nolint: gas
+			uintptr(unsafe.Pointer(&dimensions)), //nolint: gas
 			0, 0, 0,
 		); err == 0 {
 			if dimensions[1] == 0 {
diff --git a/help.go b/help.go
index 4fb7700..cf5a912 100644
--- a/help.go
+++ b/help.go
@@ -490,6 +490,7 @@ func formatFlag(haveShort bool, flag *Flag) string {
 	flagString := ""
 	name := flag.Name
 	isBool := flag.IsBool()
+	isCounter := flag.IsCounter()
 	if flag.Short != 0 {
 		if isBool && flag.Tag.Negatable {
 			flagString += fmt.Sprintf("-%c, --[no-]%s", flag.Short, name)
@@ -511,7 +512,7 @@ func formatFlag(haveShort bool, flag *Flag) string {
 			}
 		}
 	}
-	if !isBool {
+	if !isBool && !isCounter {
 		flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder())
 	}
 	return flagString
diff --git a/kong.go b/kong.go
index 255633f..76eaefe 100644
--- a/kong.go
+++ b/kong.go
@@ -336,7 +336,7 @@ func (k *Kong) applyHook(ctx *Context, name string) error {
 		binds.add(ctx, trace)
 		binds.add(trace.Node().Vars().CloneWith(k.vars))
 		binds.merge(ctx.bindings)
-		if err := callMethod(name, value, method, binds); err != nil {
+		if err := callFunction(method, binds); err != nil {
 			return err
 		}
 	}
@@ -364,7 +364,7 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er
 				continue
 			}
 			path := &Path{Flag: flag}
-			if err := callMethod(name, flag.Target, method, binds.clone().add(path)); err != nil {
+			if err := callFunction(method, binds.clone().add(path)); err != nil {
 				return next(err)
 			}
 		}
@@ -412,7 +412,7 @@ func (k *Kong) FatalIfErrorf(err error, args ...interface{}) {
 	}
 	msg := err.Error()
 	if len(args) > 0 {
-		msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error() // nolint
+		msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error() //nolint
 	}
 	// Maybe display usage information.
 	var parseErr *ParseError
@@ -439,11 +439,11 @@ func (k *Kong) LoadConfig(path string) (Resolver, error) {
 	if err != nil {
 		return nil, err
 	}
-	r, err := os.Open(path) // nolint: gas
+	r, err := os.Open(path) //nolint: gas
 	if err != nil {
 		return nil, err
 	}
-	defer r.Close() // nolint: gosec
+	defer r.Close()
 
 	return k.loader(r)
 }
diff --git a/mapper.go b/mapper.go
index a510e74..14bd896 100644
--- a/mapper.go
+++ b/mapper.go
@@ -5,7 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"math/bits"
 	"net/url"
 	"os"
@@ -17,7 +17,7 @@ import (
 
 var (
 	mapperValueType       = reflect.TypeOf((*MapperValue)(nil)).Elem()
-	boolMapperType        = reflect.TypeOf((*BoolMapper)(nil)).Elem()
+	boolMapperValueType   = reflect.TypeOf((*BoolMapperValue)(nil)).Elem()
 	jsonUnmarshalerType   = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()
 	textUnmarshalerType   = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
 	binaryUnmarshalerType = reflect.TypeOf((*encoding.BinaryUnmarshaler)(nil)).Elem()
@@ -47,15 +47,21 @@ type MapperValue interface {
 	Decode(ctx *DecodeContext) error
 }
 
+// BoolMapperValue may be implemented by fields in order to provide custom mappings for boolean values.
+type BoolMapperValue interface {
+	MapperValue
+	IsBool() bool
+}
+
 type mapperValueAdapter struct {
 	isBool bool
 }
 
 func (m *mapperValueAdapter) Decode(ctx *DecodeContext, target reflect.Value) error {
 	if target.Type().Implements(mapperValueType) {
-		return target.Interface().(MapperValue).Decode(ctx) // nolint
+		return target.Interface().(MapperValue).Decode(ctx) //nolint
 	}
-	return target.Addr().Interface().(MapperValue).Decode(ctx) // nolint
+	return target.Addr().Interface().(MapperValue).Decode(ctx) //nolint
 }
 
 func (m *mapperValueAdapter) IsBool() bool {
@@ -71,9 +77,9 @@ func (m *textUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Value
 		return err
 	}
 	if target.Type().Implements(textUnmarshalerType) {
-		return target.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) // nolint
+		return target.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) //nolint
 	}
-	return target.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) // nolint
+	return target.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)) //nolint
 }
 
 type binaryUnmarshalerAdapter struct{}
@@ -85,9 +91,9 @@ func (m *binaryUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Val
 		return err
 	}
 	if target.Type().Implements(binaryUnmarshalerType) {
-		return target.Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) // nolint
+		return target.Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) //nolint
 	}
-	return target.Addr().Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) // nolint
+	return target.Addr().Interface().(encoding.BinaryUnmarshaler).UnmarshalBinary([]byte(value)) //nolint
 }
 
 type jsonUnmarshalerAdapter struct{}
@@ -99,9 +105,9 @@ func (j *jsonUnmarshalerAdapter) Decode(ctx *DecodeContext, target reflect.Value
 		return err
 	}
 	if target.Type().Implements(jsonUnmarshalerType) {
-		return target.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) // nolint
+		return target.Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) //nolint
 	}
-	return target.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) // nolint
+	return target.Addr().Interface().(json.Unmarshaler).UnmarshalJSON([]byte(value)) //nolint
 }
 
 // A Mapper represents how a field is mapped from command-line values to Go.
@@ -136,7 +142,7 @@ type BoolMapperExt interface {
 // A MapperFunc is a single function that complies with the Mapper interface.
 type MapperFunc func(ctx *DecodeContext, target reflect.Value) error
 
-func (m MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { // nolint: revive
+func (m MapperFunc) Decode(ctx *DecodeContext, target reflect.Value) error { //nolint: revive
 	return m(ctx, target)
 }
 
@@ -194,7 +200,7 @@ func (r *Registry) ForType(typ reflect.Type) Mapper {
 	for _, impl := range []reflect.Type{typ, reflect.PtrTo(typ)} {
 		if impl.Implements(mapperValueType) {
 			// FIXME: This should pass in the bool mapper.
-			return &mapperValueAdapter{impl.Implements(boolMapperType)}
+			return &mapperValueAdapter{impl.Implements(boolMapperValueType)}
 		}
 	}
 	// Next, try explicitly registered types.
@@ -333,7 +339,7 @@ func durationDecoder() MapperFunc {
 				return fmt.Errorf("expected duration but got %q: %v", v, err)
 			}
 		case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
-			d = reflect.ValueOf(v).Convert(reflect.TypeOf(time.Duration(0))).Interface().(time.Duration) // nolint: forcetypeassert
+			d = reflect.ValueOf(v).Convert(reflect.TypeOf(time.Duration(0))).Interface().(time.Duration) //nolint: forcetypeassert
 		default:
 			return fmt.Errorf("expected duration but got %q", v)
 		}
@@ -361,7 +367,7 @@ func timeDecoder() MapperFunc {
 	}
 }
 
-func intDecoder(bits int) MapperFunc { // nolint: dupl
+func intDecoder(bits int) MapperFunc { //nolint: dupl
 	return func(ctx *DecodeContext, target reflect.Value) error {
 		t, err := ctx.Scan.PopValue("int")
 		if err != nil {
@@ -390,7 +396,7 @@ func intDecoder(bits int) MapperFunc { // nolint: dupl
 	}
 }
 
-func uintDecoder(bits int) MapperFunc { // nolint: dupl
+func uintDecoder(bits int) MapperFunc { //nolint: dupl
 	return func(ctx *DecodeContext, target reflect.Value) error {
 		t, err := ctx.Scan.PopValue("uint")
 		if err != nil {
@@ -574,6 +580,12 @@ func pathMapper(r *Registry) MapperFunc {
 		if target.Kind() == reflect.Slice {
 			return sliceDecoder(r)(ctx, target)
 		}
+		if target.Kind() == reflect.Ptr && target.Elem().Kind() == reflect.String {
+			if target.IsNil() {
+				return nil
+			}
+			target = target.Elem()
+		}
 		if target.Kind() != reflect.String {
 			return fmt.Errorf("\"path\" type must be applied to a string not %s", target.Type())
 		}
@@ -605,7 +617,7 @@ func fileMapper(r *Registry) MapperFunc {
 			file = os.Stdin
 		} else {
 			path = ExpandPath(path)
-			file, err = os.Open(path) // nolint: gosec
+			file, err = os.Open(path) //nolint: gosec
 			if err != nil {
 				return err
 			}
@@ -706,11 +718,14 @@ func fileContentMapper(r *Registry) MapperFunc {
 		var data []byte
 		if path != "-" {
 			path = ExpandPath(path)
-			data, err = ioutil.ReadFile(path) //nolint:gosec
+			data, err = os.ReadFile(path) //nolint:gosec
 		} else {
-			data, err = ioutil.ReadAll(os.Stdin)
+			data, err = io.ReadAll(os.Stdin)
 		}
 		if err != nil {
+			if info, statErr := os.Stat(path); statErr == nil && info.IsDir() {
+				return fmt.Errorf("%q exists but is a directory: %w", path, err)
+			}
 			return err
 		}
 		target.SetBytes(data)
@@ -863,7 +878,7 @@ type NamedFileContentFlag struct {
 	Contents []byte
 }
 
-func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { // nolint: revive
+func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { //nolint: revive
 	var filename string
 	err := ctx.Scan.PopValueInto("filename", &filename)
 	if err != nil {
@@ -875,7 +890,7 @@ func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { // nolint: rev
 		return nil
 	}
 	filename = ExpandPath(filename)
-	data, err := ioutil.ReadFile(filename) // nolint: gosec
+	data, err := os.ReadFile(filename) //nolint: gosec
 	if err != nil {
 		return fmt.Errorf("failed to open %q: %v", filename, err)
 	}
@@ -887,7 +902,7 @@ func (f *NamedFileContentFlag) Decode(ctx *DecodeContext) error { // nolint: rev
 // FileContentFlag is a flag value that loads a file's contents into its value.
 type FileContentFlag []byte
 
-func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: revive
+func (f *FileContentFlag) Decode(ctx *DecodeContext) error { //nolint: revive
 	var filename string
 	err := ctx.Scan.PopValueInto("filename", &filename)
 	if err != nil {
@@ -899,7 +914,7 @@ func (f *FileContentFlag) Decode(ctx *DecodeContext) error { // nolint: revive
 		return nil
 	}
 	filename = ExpandPath(filename)
-	data, err := ioutil.ReadFile(filename) // nolint: gosec
+	data, err := os.ReadFile(filename) //nolint: gosec
 	if err != nil {
 		return fmt.Errorf("failed to open %q: %v", filename, err)
 	}
diff --git a/model.go b/model.go
index 793cf97..8d1f82f 100644
--- a/model.go
+++ b/model.go
@@ -162,6 +162,16 @@ func (n *Node) Summary() string {
 	} else if len(n.Children) > 0 {
 		summary += " <command>"
 	}
+	allFlags := n.Flags
+	if n.Parent != nil {
+		allFlags = append(allFlags, n.Parent.Flags...)
+	}
+	for _, flag := range allFlags {
+		if !flag.Required {
+			summary += " [flags]"
+			break
+		}
+	}
 	return summary
 }
 
@@ -368,9 +378,9 @@ func (v *Value) Reset() error {
 	v.Target.Set(reflect.Zero(v.Target.Type()))
 	if len(v.Tag.Envs) != 0 {
 		for _, env := range v.Tag.Envs {
-			envar := os.Getenv(env)
+			envar, ok := os.LookupEnv(env)
 			// Parse the first non-empty ENV in the list
-			if envar != "" {
+			if ok {
 				err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target)
 				if err != nil {
 					return fmt.Errorf("%s (from envar %s=%q)", err, env, envar)
@@ -397,6 +407,7 @@ type Flag struct {
 	Xor         []string
 	PlaceHolder string
 	Envs        []string
+	Aliases     []string
 	Short       rune
 	Hidden      bool
 	Negated     bool
@@ -490,6 +501,9 @@ func reflectValueIsZero(v reflect.Value) bool {
 	default:
 		// This should never happens, but will act as a safeguard for
 		// later, as a default value doesn't makes sense here.
-		panic(&reflect.ValueError{"reflect.Value.IsZero", v.Kind()})
+		panic(&reflect.ValueError{
+			Method: "reflect.Value.IsZero",
+			Kind:   v.Kind(),
+		})
 	}
 }
diff --git a/options.go b/options.go
index 8d2893c..d01aeec 100644
--- a/options.go
+++ b/options.go
@@ -20,7 +20,7 @@ type Option interface {
 // OptionFunc is function that adheres to the Option interface.
 type OptionFunc func(k *Kong) error
 
-func (o OptionFunc) Apply(k *Kong) error { return o(k) } // nolint: revive
+func (o OptionFunc) Apply(k *Kong) error { return o(k) } //nolint: revive
 
 // Vars sets the variables to use for interpolation into help strings and default values.
 //
@@ -287,7 +287,7 @@ func AutoGroup(format func(parent Visitable, flag *Flag) *Group) Option {
 // See also ExplicitGroups for a more structured alternative.
 type Groups map[string]string
 
-func (g Groups) Apply(k *Kong) error { // nolint: revive
+func (g Groups) Apply(k *Kong) error { //nolint: revive
 	for key, info := range g {
 		lines := strings.Split(info, "\n")
 		title := strings.TrimSpace(lines[0])
diff --git a/renovate.json5 b/renovate.json5
new file mode 100644
index 0000000..561d59f
--- /dev/null
+++ b/renovate.json5
@@ -0,0 +1,18 @@
+{
+  $schema: "https://docs.renovatebot.com/renovate-schema.json",
+  extends: [
+    "config:recommended",
+    ":semanticCommits",
+    ":semanticCommitTypeAll(chore)",
+    ":semanticCommitScope(deps)",
+    "group:allNonMajor",
+    "schedule:earlyMondays", // Run once a week.
+  ],
+  packageRules: [
+    {
+      "matchPackageNames": ["golangci-lint"],
+      "matchManagers": ["hermit"],
+      "enabled": false
+    },
+  ]
+}
diff --git a/resolver.go b/resolver.go
index ac1de1f..05be7f6 100644
--- a/resolver.go
+++ b/resolver.go
@@ -22,10 +22,10 @@ type ResolverFunc func(context *Context, parent *Path, flag *Flag) (interface{},
 
 var _ Resolver = ResolverFunc(nil)
 
-func (r ResolverFunc) Resolve(context *Context, parent *Path, flag *Flag) (interface{}, error) { // nolint: revive
+func (r ResolverFunc) Resolve(context *Context, parent *Path, flag *Flag) (interface{}, error) { //nolint: revive
 	return r(context, parent, flag)
 }
-func (r ResolverFunc) Validate(app *Application) error { return nil } // nolint: revive
+func (r ResolverFunc) Validate(app *Application) error { return nil } //nolint: revive
 
 // JSON returns a Resolver that retrieves values from a JSON source.
 //
diff --git a/scanner.go b/scanner.go
index 1766c4b..c8a8bd6 100644
--- a/scanner.go
+++ b/scanner.go
@@ -82,7 +82,7 @@ func (t Token) InferredType() TokenType {
 		return t.Type
 	}
 	if v, ok := t.Value.(string); ok {
-		if strings.HasPrefix(v, "--") { // nolint: gocritic
+		if strings.HasPrefix(v, "--") { //nolint: gocritic
 			return FlagToken
 		} else if v == "-" {
 			return PositionalArgumentToken
@@ -109,7 +109,7 @@ func (t Token) IsValue() bool {
 //
 // For example, the token "--foo=bar" will be split into the following by the parser:
 //
-// 		[{FlagToken, "foo"}, {FlagValueToken, "bar"}]
+//	[{FlagToken, "foo"}, {FlagValueToken, "bar"}]
 type Scanner struct {
 	args []Token
 }
diff --git a/tag.go b/tag.go
index f99059b..3e37c19 100644
--- a/tag.go
+++ b/tag.go
@@ -62,7 +62,7 @@ type tagChars struct {
 var kongChars = tagChars{sep: ',', quote: '\'', assign: '=', needsUnquote: false}
 var bareChars = tagChars{sep: ' ', quote: '"', assign: ':', needsUnquote: true}
 
-// nolint:gocyclo
+//nolint:gocyclo
 func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) {
 	d := map[string][]string{}
 	key := []rune{}
@@ -203,7 +203,7 @@ func parseTag(parent reflect.Value, ft reflect.StructField) (*Tag, error) {
 	return t, nil
 }
 
-func hydrateTag(t *Tag, typ reflect.Type) error { // nolint: gocyclo
+func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo
 	var typeName string
 	var isBool bool
 	var isBoolPtr bool
diff --git a/util.go b/util.go
index 50b1dfe..8b70664 100644
--- a/util.go
+++ b/util.go
@@ -17,7 +17,7 @@ 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)) // nolint
+	path := string(ctx.FlagValue(trace.Flag).(ConfigFlag)) //nolint
 	resolver, err := kong.LoadConfig(path)
 	if err != nil {
 		return err


Test patch:
diff --git a/config_test.go b/config_test.go
index 0d7f2cc..15d6eec 100644
--- a/config_test.go
+++ b/config_test.go
@@ -2,7 +2,6 @@ package kong_test
 
 import (
 	"encoding/json"
-	"io/ioutil"
 	"os"
 	"testing"
 
@@ -45,9 +44,9 @@ func TestConfigValidation(t *testing.T) {
 
 func makeConfig(t *testing.T, config interface{}) (path string, cleanup func()) {
 	t.Helper()
-	w, err := ioutil.TempFile("", "")
+	w, err := os.CreateTemp("", "")
 	assert.NoError(t, err)
-	defer w.Close() // nolint: gosec
+	defer w.Close()
 	err = json.NewEncoder(w).Encode(config)
 	assert.NoError(t, err)
 	return w.Name(), func() { os.Remove(w.Name()) }
diff --git a/defaults_test.go b/defaults_test.go
index c78d334..fdbbc4a 100644
--- a/defaults_test.go
+++ b/defaults_test.go
@@ -24,6 +24,7 @@ func TestApplyDefaults(t *testing.T) {
 			expected: CLI{Str: "str", Duration: time.Second}},
 	}
 	for _, tt := range tests {
+		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			err := ApplyDefaults(&tt.target)
 			assert.NoError(t, err)
diff --git a/global_test.go b/global_test.go
index 86c46de..d48358f 100644
--- a/global_test.go
+++ b/global_test.go
@@ -21,7 +21,7 @@ func TestParseHandlingBadBuild(t *testing.T) {
 
 	defer func() {
 		if r := recover(); r != nil {
-			assert.Equal(t, "fail=' is not quoted properly", r.(error).Error()) // nolint
+			assert.Equal(t, "fail=' is not quoted properly", r.(error).Error()) //nolint
 		}
 	}()
 
diff --git a/help_test.go b/help_test.go
index 2c94660..38080a3 100644
--- a/help_test.go
+++ b/help_test.go
@@ -51,7 +51,7 @@ func TestHelpOptionalArgs(t *testing.T) {
 		assert.NoError(t, err)
 	})
 	assert.True(t, exited)
-	expected := `Usage: test-app [<one> [<two>]]
+	expected := `Usage: test-app [<one> [<two>]] [flags]
 
 Arguments:
   [<one>]    One optional arg.
@@ -105,7 +105,7 @@ func TestHelp(t *testing.T) {
 			assert.NoError(t, err)
 		})
 		assert.True(t, exited)
-		expected := `Usage: test-app --required <command>
+		expected := `Usage: test-app --required <command> [flags]
 
 A test app.
 
@@ -120,13 +120,13 @@ Flags:
   -s, --[no-]sort            Is sortable or not.
 
 Commands:
-  one --required
+  one --required [flags]
     A subcommand.
 
-  two <three> --required --required-two --required-three
+  two <three> --required --required-two --required-three [flags]
     Sub-sub-arg.
 
-  two four --required --required-two
+  two four --required --required-two [flags]
     Sub-sub-command.
 
 Run "test-app <command> --help" for more information on a command.
@@ -144,7 +144,7 @@ Run "test-app <command> --help" for more information on a command.
 			assert.NoError(t, err)
 		})
 		assert.True(t, exited)
-		expected := `Usage: test-app two <three> --required --required-two --required-three
+		expected := `Usage: test-app two <three> --required --required-two --required-three [flags]
 
 Sub-sub-arg.
 
@@ -215,18 +215,18 @@ func TestFlagsLast(t *testing.T) {
 			assert.NoError(t, err)
 		})
 		assert.True(t, exited)
-		expected := `Usage: test-app --required <command>
+		expected := `Usage: test-app --required <command> [flags]
 
 A test app.
 
 Commands:
-  one --required
+  one --required [flags]
     A subcommand.
 
-  two <three> --required --required-two --required-three
+  two <three> --required --required-two --required-three [flags]
     Sub-sub-arg.
 
-  two four --required --required-two
+  two four --required --required-two [flags]
     Sub-sub-command.
 
 Flags:
@@ -253,7 +253,7 @@ Run "test-app <command> --help" for more information on a command.
 			assert.NoError(t, err)
 		})
 		assert.True(t, exited)
-		expected := `Usage: test-app two <three> --required --required-two --required-three
+		expected := `Usage: test-app two <three> --required --required-two --required-three [flags]
 
 Sub-sub-arg.
 
@@ -320,7 +320,7 @@ func TestHelpTree(t *testing.T) {
 			assert.NoError(t, err)
 		})
 		assert.True(t, exited)
-		expected := `Usage: test-app <command>
+		expected := `Usage: test-app <command> [flags]
 
 A test app.
 
@@ -353,7 +353,7 @@ Run "test-app <command> --help" for more information on a command.
 			assert.NoError(t, err)
 		})
 		assert.True(t, exited)
-		expected := `Usage: test-app one (un,uno) <command>
+		expected := `Usage: test-app one (un,uno) <command> [flags]
 
 subcommand one
 
@@ -414,7 +414,7 @@ func TestHelpCompactNoExpand(t *testing.T) {
 			assert.NoError(t, err)
 		})
 		assert.True(t, exited)
-		expected := `Usage: test-app <command>
+		expected := `Usage: test-app <command> [flags]
 
 A test app.
 
@@ -443,7 +443,7 @@ Run "test-app <command> --help" for more information on a command.
 			assert.NoError(t, err)
 		})
 		assert.True(t, exited)
-		expected := `Usage: test-app one (un,uno) <command>
+		expected := `Usage: test-app one (un,uno) <command> [flags]
 
 subcommand one
 
@@ -600,14 +600,14 @@ func TestAutoGroup(t *testing.T) {
 			if node, ok := parent.(*kong.Node); ok {
 				return &kong.Group{
 					Key:   node.Name,
-					Title: strings.Title(node.Name) + " flags:", // nolint
+					Title: strings.Title(node.Name) + " flags:", //nolint
 				}
 			}
 			return nil
 		}),
 	)
 	_, _ = app.Parse([]string{"--help", "two"})
-	assert.Equal(t, `Usage: test two
+	assert.Equal(t, `Usage: test two [flags]
 
 A non grouped subcommand.
 
@@ -691,7 +691,7 @@ func TestHelpGrouping(t *testing.T) {
 			assert.True(t, exited)
 			assert.NoError(t, err)
 		})
-		expected := `Usage: test-app <command>
+		expected := `Usage: test-app <command> [flags]
 
 A test app.
 
@@ -710,26 +710,26 @@ Group B
   --grouped-b-string=STRING    A string flag grouped in B.
 
 Commands:
-  two
+  two [flags]
     A non grouped subcommand.
 
 Group title taken from the kong.ExplicitGroups option
   A group header
 
-  one thing <arg>
+  one thing <arg> [flags]
     subcommand thing
 
-  one <other>
+  one <other> [flags]
     subcommand other
 
-  three
+  three [flags]
     Another subcommand grouped in A.
 
 Group B
-  one <stuff>
+  one <stuff> [flags]
     subcommand stuff
 
-  four
+  four [flags]
     Another subcommand grouped in B.
 
 Run "test-app <command> --help" for more information on a command.
@@ -747,7 +747,7 @@ Run "test-app <command> --help" for more information on a command.
 			assert.NoError(t, err)
 			assert.True(t, exited)
 		})
-		expected := `Usage: test-app two
+		expected := `Usage: test-app two [flags]
 
 A non grouped subcommand.
 
@@ -796,7 +796,7 @@ func TestUsageOnError(t *testing.T) {
 	_, err := p.Parse([]string{})
 	p.FatalIfErrorf(err)
 
-	expected := `Usage: test --flag=STRING
+	expected := `Usage: test --flag=STRING [flags]
 
 Some description.
 
@@ -824,7 +824,7 @@ func TestShortUsageOnError(t *testing.T) {
 	assert.Error(t, err)
 	p.FatalIfErrorf(err)
 
-	expected := `Usage: test --flag=STRING
+	expected := `Usage: test --flag=STRING [flags]
 Run "test --help" for more information.
 
 test: error: missing flags: --flag=STRING
diff --git a/helpwrap1.19_test.go b/helpwrap1.19_test.go
index 7b52f93..9034e59 100644
--- a/helpwrap1.19_test.go
+++ b/helpwrap1.19_test.go
@@ -32,7 +32,7 @@ func TestCustomWrap(t *testing.T) {
 
 	_, err := app.Parse([]string{"--help"})
 	assert.NoError(t, err)
-	expected := `Usage: test-app
+	expected := `Usage: test-app [flags]
 
 A test app.
 
diff --git a/kong_test.go b/kong_test.go
index 72c73cf..bd1ecfa 100644
--- a/kong_test.go
+++ b/kong_test.go
@@ -495,6 +495,7 @@ func TestHooks(t *testing.T) {
 	p := mustNew(t, &cli, kong.Bind(ctx))
 
 	for _, test := range tests {
+		test := test
 		*ctx = hookContext{}
 		cli.One = hookCmd{}
 		t.Run(test.name, func(t *testing.T) {
@@ -517,6 +518,16 @@ func TestShort(t *testing.T) {
 	assert.Equal(t, "hello", cli.String)
 }
 
+func TestAlias(t *testing.T) {
+	var cli struct {
+		String string `aliases:"str"`
+	}
+	app := mustNew(t, &cli)
+	_, err := app.Parse([]string{"--str", "hello"})
+	assert.NoError(t, err)
+	assert.Equal(t, "hello", cli.String)
+}
+
 func TestDuplicateFlagChoosesLast(t *testing.T) {
 	var cli struct {
 		Flag int
@@ -641,6 +652,23 @@ func TestRun(t *testing.T) {
 	assert.Equal(t, "argping", cli.Three.SubCommand.Arg)
 }
 
+type failCmd struct{}
+
+func (f failCmd) Run() error {
+	return errors.New("this command failed")
+}
+
+func TestPassesThroughOriginalCommandError(t *testing.T) {
+	var cli struct {
+		Fail failCmd `kong:"cmd"`
+	}
+	p := mustNew(t, &cli)
+	ctx, _ := p.Parse([]string{"fail"})
+	err := ctx.Run()
+	assert.Error(t, err)
+	assert.Equal(t, err.Error(), "this command failed")
+}
+
 func TestInterpolationIntoModel(t *testing.T) {
 	var cli struct {
 		Flag    string `default:"${default_value}" help:"Help, I need ${somebody}" enum:"${enum}"`
@@ -736,7 +764,7 @@ func TestEmbedInterface(t *testing.T) {
 	_, err := p.Parse([]string{"--some-flag=foo", "--flag=yes"})
 	assert.NoError(t, err)
 	assert.Equal(t, "foo", cli.SomeFlag)
-	assert.Equal(t, "yes", cli.TestInterface.(*TestImpl).Flag) // nolint
+	assert.Equal(t, "yes", cli.TestInterface.(*TestImpl).Flag) //nolint
 }
 
 func TestExcludedField(t *testing.T) {
@@ -1214,7 +1242,7 @@ func TestValidateApp(t *testing.T) {
 	cli := validateCli{}
 	p := mustNew(t, &cli)
 	_, err := p.Parse([]string{})
-	assert.EqualError(t, err, "test: app error")
+	assert.EqualError(t, err, "app error")
 }
 
 func TestValidateCmd(t *testing.T) {
@@ -1303,6 +1331,24 @@ func TestDuplicateShortflags(t *testing.T) {
 	assert.EqualError(t, err, "<anonymous struct>.Flag2: duplicate short flag -t")
 }
 
+func TestDuplicateAliases(t *testing.T) {
+	cli1 := struct {
+		Flag1 string `aliases:"flag"`
+		Flag2 string `aliases:"flag"`
+	}{}
+	_, err := kong.New(&cli1)
+	assert.EqualError(t, err, "<anonymous struct>.Flag2: duplicate flag --flag")
+}
+
+func TestDuplicateAliasLong(t *testing.T) {
+	cli2 := struct {
+		Flag  string ``
+		Flag2 string `aliases:"flag"` // duplicates Flag
+	}{}
+	_, err := kong.New(&cli2)
+	assert.EqualError(t, err, "<anonymous struct>.Flag2: duplicate flag --flag")
+}
+
 func TestDuplicateNestedShortFlags(t *testing.T) {
 	cli := struct {
 		Flag1 bool `short:"t"`
@@ -1335,16 +1381,16 @@ func TestHydratePointerCommandsAndEmbeds(t *testing.T) {
 	assert.Equal(t, &embed{Embed: true}, cli.Embed)
 }
 
-// nolint
+//nolint:revive
 type testIgnoreFields struct {
 	Foo struct {
 		Bar bool
 		Sub struct {
 			SubFlag1     bool `kong:"name=subflag1"`
-			XXX_SubFlag2 bool `kong:"name=subflag2"`
+			XXX_SubFlag2 bool `kong:"name=subflag2"` //nolint:stylecheck
 		} `kong:"cmd"`
 	} `kong:"cmd"`
-	XXX_Baz struct {
+	XXX_Baz struct { //nolint:stylecheck
 		Boo bool
 	} `kong:"cmd,name=baz"`
 }
@@ -1530,7 +1576,7 @@ func TestPassthroughCmdOnlyArgs(t *testing.T) {
 		} `cmd:"" passthrough:""`
 	}
 	_, err := kong.New(&cli)
-	assert.EqualError(t, err, "<anonymous struct>.Command: passthrough command command [<args> ...] must not have subcommands or flags")
+	assert.EqualError(t, err, "<anonymous struct>.Command: passthrough command command [<args> ...] [flags] must not have subcommands or flags")
 }
 
 func TestPassthroughCmdOnlyStringArgs(t *testing.T) {
@@ -1911,8 +1957,8 @@ func TestBoolPtrNil(t *testing.T) {
 
 func TestUnsupportedPtr(t *testing.T) {
 	type Foo struct {
-		x int // nolint
-		y int // nolint
+		x int //nolint
+		y int //nolint
 	}
 
 	var cli struct {
diff --git a/mapper_test.go b/mapper_test.go
index 81818fc..e054712 100644
--- a/mapper_test.go
+++ b/mapper_test.go
@@ -4,12 +4,11 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
 	"math"
 	"net/url"
 	"os"
+	"path/filepath"
 	"reflect"
-	"runtime"
 	"strings"
 	"testing"
 	"time"
@@ -269,7 +268,7 @@ func TestFileContentFlag(t *testing.T) {
 	var cli struct {
 		File kong.FileContentFlag
 	}
-	f, err := ioutil.TempFile("", "")
+	f, err := os.CreateTemp("", "")
 	assert.NoError(t, err)
 	defer os.Remove(f.Name())
 	fmt.Fprint(f, "hello world")
@@ -283,7 +282,7 @@ func TestNamedFileContentFlag(t *testing.T) {
 	var cli struct {
 		File kong.NamedFileContentFlag
 	}
-	f, err := ioutil.TempFile("", "")
+	f, err := os.CreateTemp("", "")
 	assert.NoError(t, err)
 	defer os.Remove(f.Name())
 	fmt.Fprint(f, "hello world")
@@ -356,14 +355,14 @@ func TestNumbers(t *testing.T) {
 		_, err := p.Parse([]string{
 			"--f-32", fmt.Sprintf("%v", math.MaxFloat32),
 			"--f-64", fmt.Sprintf("%v", math.MaxFloat64),
-			"--i-8", fmt.Sprintf("%v", int8(math.MaxInt8)),
-			"--i-16", fmt.Sprintf("%v", int16(math.MaxInt16)),
-			"--i-32", fmt.Sprintf("%v", int32(math.MaxInt32)),
-			"--i-64", fmt.Sprintf("%v", int64(math.MaxInt64)),
-			"--u-8", fmt.Sprintf("%v", uint8(math.MaxUint8)),
-			"--u-16", fmt.Sprintf("%v", uint16(math.MaxUint16)),
-			"--u-32", fmt.Sprintf("%v", uint32(math.MaxUint32)),
-			"--u-64", fmt.Sprintf("%v", uint64(math.MaxUint64)),
+			"--i-8", fmt.Sprintf("%v", int8(math.MaxInt8)), //nolint:perfsprint // want int8
+			"--i-16", fmt.Sprintf("%v", int16(math.MaxInt16)), //nolint:perfsprint // want int16
+			"--i-32", fmt.Sprintf("%v", int32(math.MaxInt32)), //nolint:perfsprint // want int32
+			"--i-64", fmt.Sprintf("%v", int64(math.MaxInt64)), //nolint:perfsprint // want int64
+			"--u-8", fmt.Sprintf("%v", uint8(math.MaxUint8)), //nolint:perfsprint // want uint8
+			"--u-16", fmt.Sprintf("%v", uint16(math.MaxUint16)), //nolint:perfsprint // want uint16
+			"--u-32", fmt.Sprintf("%v", uint32(math.MaxUint32)), //nolint:perfsprint // want uint32
+			"--u-64", fmt.Sprintf("%v", uint64(math.MaxUint64)), //nolint:perfsprint // want uint64
 		})
 		assert.NoError(t, err)
 		assert.Equal(t, CLI{
@@ -460,15 +459,13 @@ func TestFileMapper(t *testing.T) {
 	_ = cli.File.Close()
 	_, err = p.Parse([]string{"testdata/missing.txt"})
 	assert.Error(t, err)
-	if runtime.GOOS == "windows" {
-		assert.Contains(t, err.Error(), "missing.txt: The system cannot find the file specified.")
-	} else {
-		assert.Contains(t, err.Error(), "missing.txt: no such file or directory")
-	}
+	assert.Contains(t, err.Error(), "missing.txt:")
+	assert.IsError(t, err, os.ErrNotExist)
 	_, err = p.Parse([]string{"-"})
 	assert.NoError(t, err)
 	assert.Equal(t, os.Stdin, cli.File)
 }
+
 func TestFileContentMapper(t *testing.T) {
 	type CLI struct {
 		File []byte `type:"filecontent"`
@@ -481,13 +478,58 @@ func TestFileContentMapper(t *testing.T) {
 	p = mustNew(t, &cli)
 	_, err = p.Parse([]string{"--file", "testdata/missing.txt"})
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "missing.txt: no such file or directory")
+	assert.Contains(t, err.Error(), "missing.txt:")
+	assert.IsError(t, err, os.ErrNotExist)
 	p = mustNew(t, &cli)
-	_, err = p.Parse([]string{"--file", "testdata/"})
+
+	_, err = p.Parse([]string{"--file", "testdata"})
 	assert.Error(t, err)
 	assert.Contains(t, err.Error(), "is a directory")
 }
 
+func TestPathMapperUsingStringPointer(t *testing.T) {
+	type CLI struct {
+		Path *string `type:"path"`
+	}
+	var cli CLI
+
+	t.Run("With value", func(t *testing.T) {
+		p := mustNew(t, &cli)
+		_, err := p.Parse([]string{"--path", "/foo/bar"})
+		assert.NoError(t, err)
+		assert.NotZero(t, cli.Path)
+		assert.Equal(t, "/foo/bar", *cli.Path)
+	})
+
+	t.Run("Zero value", func(t *testing.T) {
+		p := mustNew(t, &cli)
+		_, err := p.Parse([]string{"--path", ""})
+		assert.NoError(t, err)
+		assert.NotZero(t, cli.Path)
+		wd, err := os.Getwd()
+		assert.NoError(t, err)
+		assert.Equal(t, wd, *cli.Path)
+	})
+
+	t.Run("Without value", func(t *testing.T) {
+		p := mustNew(t, &cli)
+		_, err := p.Parse([]string{"--"})
+		assert.NoError(t, err)
+		assert.Equal(t, nil, cli.Path)
+	})
+
+	t.Run("Non-string pointer", func(t *testing.T) {
+		type CLI struct {
+			Path *any `type:"path"`
+		}
+		var cli CLI
+		p := mustNew(t, &cli)
+		_, err := p.Parse([]string{"--path", ""})
+		assert.Error(t, err)
+		assert.Contains(t, err.Error(), `"path" type must be applied to a string`)
+	})
+}
+
 //nolint:dupl
 func TestExistingFileMapper(t *testing.T) {
 	type CLI struct {
@@ -501,7 +543,8 @@ func TestExistingFileMapper(t *testing.T) {
 	p = mustNew(t, &cli)
 	_, err = p.Parse([]string{"--file", "testdata/missing.txt"})
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "missing.txt: no such file or directory")
+	assert.Contains(t, err.Error(), "missing.txt:")
+	assert.IsError(t, err, os.ErrNotExist)
 	p = mustNew(t, &cli)
 	_, err = p.Parse([]string{"--file", "testdata/"})
 	assert.Error(t, err)
@@ -514,7 +557,7 @@ func TestExistingFileMapperDefaultMissing(t *testing.T) {
 	}
 	var cli CLI
 	p := mustNew(t, &cli)
-	file := "testdata/file.txt"
+	file := filepath.Join("testdata", "file.txt")
 	_, err := p.Parse([]string{"--file", file})
 	assert.NoError(t, err)
 	assert.NotZero(t, cli.File)
@@ -522,7 +565,8 @@ func TestExistingFileMapperDefaultMissing(t *testing.T) {
 	p = mustNew(t, &cli)
 	_, err = p.Parse([]string{})
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "missing.txt: no such file or directory")
+	assert.Contains(t, err.Error(), "missing.txt:")
+	assert.IsError(t, err, os.ErrNotExist)
 }
 
 func TestExistingFileMapperDefaultMissingCmds(t *testing.T) {
@@ -536,7 +580,7 @@ func TestExistingFileMapperDefaultMissingCmds(t *testing.T) {
 		} `cmd:""`
 	}
 	var cli CLI
-	file := "testdata/file.txt"
+	file := filepath.Join("testdata", "file.txt")
 	p := mustNew(t, &cli)
 	_, err := p.Parse([]string{"cmd-a", "--file-a", file, "--file-b", file})
 	assert.NoError(t, err)
@@ -547,7 +591,8 @@ func TestExistingFileMapperDefaultMissingCmds(t *testing.T) {
 	p = mustNew(t, &cli)
 	_, err = p.Parse([]string{"cmd-a", "--file-a", file})
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "bbb-missing.txt: no such file or directory")
+	assert.Contains(t, err.Error(), "bbb-missing.txt:")
+	assert.IsError(t, err, os.ErrNotExist)
 }
 
 //nolint:dupl
@@ -563,7 +608,8 @@ func TestExistingDirMapper(t *testing.T) {
 	p = mustNew(t, &cli)
 	_, err = p.Parse([]string{"--dir", "missingdata/"})
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "missingdata: no such file or directory")
+	assert.Contains(t, err.Error(), "missingdata:")
+	assert.IsError(t, err, os.ErrNotExist)
 	p = mustNew(t, &cli)
 	_, err = p.Parse([]string{"--dir", "testdata/file.txt"})
 	assert.Error(t, err)
@@ -584,7 +630,8 @@ func TestExistingDirMapperDefaultMissing(t *testing.T) {
 	p = mustNew(t, &cli)
 	_, err = p.Parse([]string{})
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "missing-dir: no such file or directory")
+	assert.Contains(t, err.Error(), "missing-dir:")
+	assert.IsError(t, err, os.ErrNotExist)
 }
 
 func TestExistingDirMapperDefaultMissingCmds(t *testing.T) {
@@ -609,7 +656,8 @@ func TestExistingDirMapperDefaultMissingCmds(t *testing.T) {
 	p = mustNew(t, &cli)
 	_, err = p.Parse([]string{"cmd-a", "--dir-a", dir})
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "bbb-missing-dir: no such file or directory")
+	assert.Contains(t, err.Error(), "bbb-missing-dir:")
+	assert.IsError(t, err, os.ErrNotExist)
 }
 
 func TestMapperPlaceHolder(t *testing.T) {
@@ -632,8 +680,7 @@ func TestMapperPlaceHolder(t *testing.T) {
 	assert.Contains(t, b.String(), "--flag=/a/b/c")
 }
 
-type testMapperWithPlaceHolder struct {
-}
+type testMapperWithPlaceHolder struct{}
 
 func (t testMapperWithPlaceHolder) Decode(ctx *kong.DecodeContext, target reflect.Value) error {
 	target.SetString("hi")
diff --git a/mapper_windows_test.go b/mapper_windows_test.go
index 2a51fee..ba2913f 100644
--- a/mapper_windows_test.go
+++ b/mapper_windows_test.go
@@ -33,11 +33,12 @@ func TestWindowsFileMapper(t *testing.T) {
 	p := mustNew(t, &cli)
 	_, err := p.Parse([]string{"testdata\\file.txt"})
 	assert.NoError(t, err)
-	assert.NotNil(t, cli.File)
+	assert.NotZero(t, cli.File, "File should not be nil")
 	_ = cli.File.Close()
 	_, err = p.Parse([]string{"testdata\\missing.txt"})
 	assert.Error(t, err)
-	assert.Contains(t, err.Error(), "missing.txt: The system cannot find the file specified.")
+	assert.Contains(t, err.Error(), "missing.txt:")
+	assert.IsError(t, err, os.ErrNotExist)
 	_, err = p.Parse([]string{"-"})
 	assert.NoError(t, err)
 	assert.Equal(t, os.Stdin, cli.File)
diff --git a/options_test.go b/options_test.go
index 2dbd0ba..ca41720 100644
--- a/options_test.go
+++ b/options_test.go
@@ -30,7 +30,7 @@ func TestBindTo(t *testing.T) {
 
 	saw := ""
 	method := func(i iface) error {
-		saw = string(i.(impl)) // nolint
+		saw = string(i.(impl)) //nolint
 		return nil
 	}
 
@@ -38,7 +38,7 @@ func TestBindTo(t *testing.T) {
 
 	p, err := New(&cli, BindTo(impl("foo"), (*iface)(nil)))
 	assert.NoError(t, err)
-	err = callMethod("method", reflect.ValueOf(impl("??")), reflect.ValueOf(method), p.bindings)
+	err = callFunction(reflect.ValueOf(method), p.bindings)
 	assert.NoError(t, err)
 	assert.Equal(t, "foo", saw)
 }
@@ -50,7 +50,7 @@ func TestInvalidCallback(t *testing.T) {
 
 	saw := ""
 	method := func(i iface) string {
-		saw = string(i.(impl)) // nolint
+		saw = string(i.(impl)) //nolint
 		return saw
 	}
 
@@ -58,8 +58,8 @@ func TestInvalidCallback(t *testing.T) {
 
 	p, err := New(&cli, BindTo(impl("foo"), (*iface)(nil)))
 	assert.NoError(t, err)
-	err = callMethod("method", reflect.ValueOf(impl("??")), reflect.ValueOf(method), p.bindings)
-	assert.EqualError(t, err, `kong.impl.method(): return value of func(kong.iface) string must implement "error"`)
+	err = callFunction(reflect.ValueOf(method), p.bindings)
+	assert.EqualError(t, err, `return value of func(kong.iface) string must implement "error"`)
 }
 
 type zrror struct{}
@@ -75,7 +75,7 @@ func TestCallbackCustomError(t *testing.T) {
 
 	saw := ""
 	method := func(i iface) *zrror {
-		saw = string(i.(impl)) // nolint
+		saw = string(i.(impl)) //nolint
 		return nil
 	}
 
@@ -83,7 +83,7 @@ func TestCallbackCustomError(t *testing.T) {
 
 	p, err := New(&cli, BindTo(impl("foo"), (*iface)(nil)))
 	assert.NoError(t, err)
-	err = callMethod("method", reflect.ValueOf(impl("??")), reflect.ValueOf(method), p.bindings)
+	err = callFunction(reflect.ValueOf(method), p.bindings)
 	assert.NoError(t, err)
 	assert.Equal(t, "foo", saw)
 }
diff --git a/util_test.go b/util_test.go
index 2457ed8..5ec0131 100644
--- a/util_test.go
+++ b/util_test.go
@@ -1,7 +1,6 @@
 package kong
 
 import (
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -17,10 +16,10 @@ func TestConfigFlag(t *testing.T) {
 		Flag   string
 	}
 
-	w, err := ioutil.TempFile("", "")
+	w, err := os.CreateTemp("", "")
 	assert.NoError(t, err)
 	defer os.Remove(w.Name())
-	w.WriteString(`{"flag": "hello world"}`) // nolint: errcheck
+	w.WriteString(`{"flag": "hello world"}`) //nolint: errcheck
 	w.Close()
 
 	p := Must(&cli, Configuration(JSON))
@@ -48,7 +47,7 @@ func TestVersionFlag(t *testing.T) {
 func TestChangeDirFlag(t *testing.T) {
 	cwd, err := os.Getwd()
 	assert.NoError(t, err)
-	defer os.Chdir(cwd) // nolint: errcheck
+	defer os.Chdir(cwd) //nolint: errcheck
 
 	dir := t.TempDir()
 	file := filepath.Join(dir, "out.txt")