Repository: alecthomas/kong
Pull request #387 (2023-10-17 21:13:49)

Remove extra newline when message contains trailing newlines
If there is trailing newline, `strings.Split` creates an empty last line in `lines`, making this function output an extra empty line at the end. Trimming trailing newlines fixes that.


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..2c5bfe3 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,18 @@ 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
+
+    - linters: [maintidx]
+      path: context\.go
+      text: 'Function name: trace'
diff --git a/README.md b/README.md
index 4110465..916ca6f 100644
--- a/README.md
+++ b/README.md
@@ -330,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))
 
@@ -566,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.                                                                                                                                                                                                                                                                                         |
@@ -733,7 +733,7 @@ All builtin Go types (as well as a bunch of useful stdlib types like `time.Time`
 The default help output is usually sufficient, but if not there are two solutions.
 
 1. Use `ConfigureHelp(HelpOptions)` to configure how help is formatted (see [HelpOptions](https://godoc.org/github.com/alecthomas/kong#HelpOptions) for details).
-2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `PrintHelp` for an example.
+2. Custom help can be wired into Kong via the `Help(HelpFunc)` option. The `HelpFunc` is passed a `Context`, which contains the parsed context for the current command-line. See the implementation of `DefaultHelpPrinter` for an example.
 3. Use `ValueFormatter(HelpValueFormatter)` if you want to just customize the help text that is accompanied by flags and arguments.
 4. Use `Groups([]Group)` if you want to customize group titles or add a header.
 
diff --git a/_examples/server/go.mod b/_examples/server/go.mod
index a5147ca..1feb248 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.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.2.2
+	github.com/gliderlabs/ssh v0.3.7
 	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.25.0
 )
diff --git a/_examples/server/go.sum b/_examples/server/go.sum
index 3693198..bf1cd72 100644
--- a/_examples/server/go.sum
+++ b/_examples/server/go.sum
@@ -1,13 +1,25 @@
+github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA=
+github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
 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/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
+github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
+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=
 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 +30,16 @@ 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/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE=
+github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 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 +54,109 @@ 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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+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/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
 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=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
 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/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 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.12.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/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+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=
+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.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+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/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
+golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
+golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
 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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 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/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/_examples/shell/help/main.go b/_examples/shell/help/main.go
index 56b8ce6..4d1d154 100644
--- a/_examples/shell/help/main.go
+++ b/_examples/shell/help/main.go
@@ -7,14 +7,16 @@ import (
 )
 
 var cli struct {
-	Flag flagWithHelp    `help:"Regular flag help"`
+	Flag flagWithHelp    `help:"${flag_help}"`
 	Echo commandWithHelp `cmd:"" help:"Regular command help"`
 }
 
 type flagWithHelp bool
 
-func (f *flagWithHelp) Help() string {
-	return "🏁 additional flag help"
+// See https://github.com/alecthomas/kong?tab=readme-ov-file#variable-interpolation
+var vars = kong.Vars{
+	"flag_help": "Extended flag help that might be too long for directly " +
+		"including in the struct tag field",
 }
 
 type commandWithHelp struct {
@@ -41,7 +43,8 @@ func main() {
 		kong.ConfigureHelp(kong.HelpOptions{
 			Compact: true,
 			Summary: false,
-		}))
+		}),
+		vars)
 	switch ctx.Command() {
 	case "echo <msg>":
 		fmt.Println(cli.Echo.Msg)
diff --git a/bin/.go-1.19.1.pkg b/bin/.go-1.22.5.pkg
similarity index 100%
rename from bin/.go-1.19.1.pkg
rename to bin/.go-1.22.5.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..5c26cb9 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.5.pkg
\ No newline at end of file
diff --git a/bin/gofmt b/bin/gofmt
index deef954..5c26cb9 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.5.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..aed1f72 100644
--- a/build.go
+++ b/build.go
@@ -51,6 +51,9 @@ type flattenedField struct {
 
 func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err error) {
 	v = reflect.Indirect(v)
+	if v.Kind() != reflect.Struct {
+		return out, nil
+	}
 	for i := 0; i < v.NumField(); i++ {
 		ft := v.Type().Field(i)
 		fv := v.Field(i)
@@ -170,6 +173,9 @@ MAIN:
 		if flag.Short != 0 {
 			delete(seenFlags, "-"+string(flag.Short))
 		}
+		for _, aflag := range flag.Aliases {
+			delete(seenFlags, "--"+aflag)
+		}
 	}
 
 	if err := validatePositionalArguments(node); err != nil {
@@ -296,6 +302,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 +317,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 8771a3e..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) {
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 0b41079..03044ec 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:
@@ -347,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
 
@@ -377,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)
 
@@ -420,12 +420,22 @@ func (c *Context) trace(node *Node) (err error) { // nolint: gocyclo
 
 		case FlagToken:
 			if err := c.parseFlag(flags, token.String()); err != nil {
-				return err
+				if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].Passthrough {
+					c.scan.Pop()
+					c.scan.PushTyped(token.String(), PositionalArgumentToken)
+				} else {
+					return err
+				}
 			}
 
 		case ShortFlagToken:
 			if err := c.parseFlag(flags, token.String()); err != nil {
-				return err
+				if isUnknownFlagError(err) && positional < len(node.Positional) && node.Positional[positional].Passthrough {
+					c.scan.Pop()
+					c.scan.PushTyped(token.String(), PositionalArgumentToken)
+				} else {
+					return err
+				}
 			}
 
 		case FlagValueToken:
@@ -684,15 +694,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.
@@ -719,9 +738,19 @@ func (c *Context) parseFlag(flags []*Flag, match string) (err error) {
 		c.Path = append(c.Path, &Path{Flag: flag})
 		return nil
 	}
-	return findPotentialCandidates(match, candidates, "unknown flag %s", match)
+	return &unknownFlagError{Cause: findPotentialCandidates(match, candidates, "unknown flag %s", match)}
 }
 
+func isUnknownFlagError(err error) bool {
+	var unknown *unknownFlagError
+	return errors.As(err, &unknown)
+}
+
+type unknownFlagError struct{ Cause error }
+
+func (e *unknownFlagError) Unwrap() error { return e.Cause }
+func (e *unknownFlagError) Error() string { return e.Cause.Error() }
+
 // Call an arbitrary function filling arguments with bound values.
 func (c *Context) Call(fn any, binds ...interface{}) (out []interface{}, err error) {
 	fv := reflect.ValueOf(fn)
@@ -874,7 +903,7 @@ func checkMissingChildren(node *Node) error {
 	if len(missing) == 1 {
 		return fmt.Errorf("expected %s", missing[0])
 	}
-	return fmt.Errorf("expected one of %s", strings.Join(missing, ",  "))
+	return fmt.Errorf("expected one of %s", strings.Join(missing, ", "))
 }
 
 // If we're missing any positionals and they're required, return an error.
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..dad659b 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.10.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..fe9dd12 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,16 @@
-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/assert/v2 v2.8.1 h1:YCxnYR6jjpfnEK5AK5SysALKdUEBPGH4Y7As6tBnDw0=
+github.com/alecthomas/assert/v2 v2.8.1/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY=
+github.com/alecthomas/assert/v2 v2.10.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 c4eda71..19ff88e 100644
--- a/kong.go
+++ b/kong.go
@@ -373,7 +373,7 @@ func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) er
 }
 
 func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...interface{}) {
-	lines := strings.Split(fmt.Sprintf(format, args...), "\n")
+	lines := strings.Split(strings.TrimRight(fmt.Sprintf(format, args...), "\n"), "\n")
 	leader := ""
 	for _, l := range leaders {
 		if l == "" {
@@ -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 c332cce..584bb00 100644
--- a/mapper.go
+++ b/mapper.go
@@ -5,7 +5,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"math/bits"
 	"net/url"
 	"os"
@@ -59,9 +59,9 @@ type mapperValueAdapter struct {
 
 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 {
@@ -77,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{}
@@ -91,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{}
@@ -105,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.
@@ -142,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)
 }
 
@@ -339,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)
 		}
@@ -367,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 {
@@ -396,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 {
@@ -540,7 +540,7 @@ func sliceDecoder(r *Registry) MapperFunc {
 		var childScanner *Scanner
 		if ctx.Value.Flag != nil {
 			t := ctx.Scan.Pop()
-			// If decoding a flag, we need an value.
+			// If decoding a flag, we need a value.
 			if t.IsEOL() {
 				return fmt.Errorf("missing value, expecting \"<arg>%c...\"", sep)
 			}
@@ -580,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())
 		}
@@ -611,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
 			}
@@ -635,7 +641,7 @@ func existingFileMapper(r *Registry) MapperFunc {
 			return err
 		}
 
-		if !ctx.Value.Active || ctx.Value.Set {
+		if !ctx.Value.Active || (ctx.Value.Set && ctx.Value.Target.Type() == target.Type()) {
 			// early return to avoid checking extra files that may not exist;
 			// this hack only works because the value provided on the cli is
 			// checked before the default value is checked (if default is set).
@@ -671,7 +677,7 @@ func existingDirMapper(r *Registry) MapperFunc {
 			return err
 		}
 
-		if !ctx.Value.Active || ctx.Value.Set {
+		if !ctx.Value.Active || (ctx.Value.Set && ctx.Value.Target.Type() == target.Type()) {
 			// early return to avoid checking extra dirs that may not exist;
 			// this hack only works because the value provided on the cli is
 			// checked before the default value is checked (if default is set).
@@ -712,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)
@@ -869,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 {
@@ -881,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)
 	}
@@ -893,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 {
@@ -905,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..3ab383b 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.
 //
@@ -89,6 +89,10 @@ type dynamicCommand struct {
 // "tags" is a list of extra tag strings to parse, in the form <key>:"<value>".
 func DynamicCommand(name, help, group string, cmd interface{}, tags ...string) Option {
 	return OptionFunc(func(k *Kong) error {
+		if run := getMethod(reflect.Indirect(reflect.ValueOf(cmd)), "Run"); !run.IsValid() {
+			return fmt.Errorf("kong: DynamicCommand %q must be a type with a 'Run' method; got %T", name, cmd)
+		}
+
 		k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{
 			name:  name,
 			help:  help,
@@ -287,7 +291,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 e7a5ba0..f3fb869 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
@@ -582,11 +593,24 @@ func TestSliceWithDisabledSeparator(t *testing.T) {
 }
 
 func TestMultilineMessage(t *testing.T) {
-	w := &bytes.Buffer{}
-	var cli struct{}
-	p := mustNew(t, &cli, kong.Writers(w, w))
-	p.Printf("hello\nworld")
-	assert.Equal(t, "test: hello\n      world\n", w.String())
+	tests := []struct {
+		name string
+		text string
+		want string
+	}{
+		{"Simple", "hello\nworld", "test: hello\n      world\n"},
+		{"WithNewline", "hello\nworld\n", "test: hello\n      world\n"},
+	}
+	for _, test := range tests {
+		test := test
+		t.Run(test.name, func(t *testing.T) {
+			w := &bytes.Buffer{}
+			var cli struct{}
+			p := mustNew(t, &cli, kong.Writers(w, w))
+			p.Printf(test.text)
+			assert.Equal(t, test.want, w.String())
+		})
+	}
 }
 
 type cmdWithRun struct {
@@ -753,7 +777,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) {
@@ -1286,6 +1310,12 @@ func (d *dynamicCommand) Run() error {
 	return nil
 }
 
+type commandFunc func() error
+
+func (cf commandFunc) Run() error {
+	return cf()
+}
+
 func TestDynamicCommands(t *testing.T) {
 	cli := struct {
 		One struct{} `cmd:"one"`
@@ -1293,9 +1323,12 @@ func TestDynamicCommands(t *testing.T) {
 	help := &strings.Builder{}
 	two := &dynamicCommand{}
 	three := &dynamicCommand{}
+	fourRan := false
+	four := commandFunc(func() error { fourRan = true; return nil })
 	p := mustNew(t, &cli,
 		kong.DynamicCommand("two", "", "", &two),
 		kong.DynamicCommand("three", "", "", three, "hidden"),
+		kong.DynamicCommand("four", "", "", &four),
 		kong.Writers(help, help),
 		kong.Exit(func(int) {}))
 	kctx, err := p.Parse([]string{"two", "--flag=flag"})
@@ -1306,8 +1339,15 @@ func TestDynamicCommands(t *testing.T) {
 	assert.NoError(t, err)
 	assert.True(t, two.ran)
 
+	kctx, err = p.Parse([]string{"four"})
+	assert.NoError(t, err)
+	assert.False(t, fourRan)
+	err = kctx.Run()
+	assert.NoError(t, err)
+	assert.True(t, fourRan)
+
 	_, err = p.Parse([]string{"--help"})
-	assert.EqualError(t, err, `expected one of "one",  "two"`)
+	assert.EqualError(t, err, `expected one of "one", "two", "four"`)
 	assert.NotContains(t, help.String(), "three", help.String())
 }
 
@@ -1320,6 +1360,38 @@ 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 TestSubCommandAliases(t *testing.T) {
+	type SubC struct {
+		Flag1 string `aliases:"flag"`
+	}
+
+	cli1 := struct {
+		Sub1 SubC `cmd:"sub1"`
+		Sub2 SubC `cmd:"sub2"`
+	}{}
+
+	_, err := kong.New(&cli1)
+	assert.NoError(t, err, "dupe aliases shouldn't error if they're in separate sub commands")
+}
+
+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"`
@@ -1352,16 +1424,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"`
 }
@@ -1483,6 +1555,54 @@ func TestEnumValidation(t *testing.T) {
 	}
 }
 
+func TestPassthroughArgs(t *testing.T) {
+	tests := []struct {
+		name    string
+		args    []string
+		flag    string
+		cmdArgs []string
+	}{
+		{
+			"NoArgs",
+			[]string{},
+			"",
+			[]string(nil),
+		},
+		{
+			"RecognizedFlagAndArgs",
+			[]string{"--flag", "foobar", "something"},
+			"foobar",
+			[]string{"something"},
+		},
+		{
+			"DashDashBeforeRecognizedFlag",
+			[]string{"--", "--flag", "foobar"},
+			"",
+			[]string{"--flag", "foobar"},
+		},
+		{
+			"UnrecognizedFlagAndArgs",
+			[]string{"--unrecognized-flag", "something"},
+			"",
+			[]string{"--unrecognized-flag", "something"},
+		},
+	}
+	for _, test := range tests {
+		test := test
+		t.Run(test.name, func(t *testing.T) {
+			var cli struct {
+				Flag string
+				Args []string `arg:"" optional:"" passthrough:""`
+			}
+			p := mustNew(t, &cli)
+			_, err := p.Parse(test.args)
+			assert.NoError(t, err)
+			assert.Equal(t, test.flag, cli.Flag)
+			assert.Equal(t, test.cmdArgs, cli.Args)
+		})
+	}
+}
+
 func TestPassthroughCmd(t *testing.T) {
 	tests := []struct {
 		name    string
@@ -1547,7 +1667,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) {
@@ -1928,8 +2048,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..113e9f5 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,60 @@ 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) {
+		pwd, err := os.Getwd()
+		assert.NoError(t, err)
+		p := mustNew(t, &cli)
+		_, err = p.Parse([]string{"--path", "."})
+		assert.NoError(t, err)
+		assert.NotZero(t, cli.Path)
+		assert.Equal(t, pwd, *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,20 +545,35 @@ 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)
 	assert.Contains(t, err.Error(), "exists but is a directory")
 }
 
+func TestExistingFileMapperSlice(t *testing.T) {
+	type CLI struct {
+		Files []string `type:"existingfile"`
+	}
+	var cli CLI
+	p := mustNew(t, &cli)
+	_, err := p.Parse([]string{"--files", "testdata/file.txt", "--files", "testdata/file.txt"})
+	assert.NoError(t, err)
+	assert.NotZero(t, cli.Files)
+	pwd, err := os.Getwd()
+	assert.NoError(t, err)
+	assert.Equal(t, []string{filepath.Join(pwd, "testdata", "file.txt"), filepath.Join(pwd, "testdata", "file.txt")}, cli.Files)
+}
+
 func TestExistingFileMapperDefaultMissing(t *testing.T) {
 	type CLI struct {
 		File string `type:"existingfile" default:"testdata/missing.txt"`
 	}
 	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 +581,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 +596,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 +607,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 +624,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 +646,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 +672,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 +696,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 bb9cdaa..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
 	}
 
@@ -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
 	}
 
@@ -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
 	}
 
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")