├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── COPYING ├── README.md ├── _examples ├── docker │ ├── README.md │ ├── commands.go │ └── main.go ├── server │ ├── README.md │ ├── console.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ ├── server_rsa_key │ └── server_rsa_key.pub └── shell │ ├── commandstring │ └── main.go │ └── help │ ├── README.md │ └── main.go ├── benchmark_test.go ├── bin ├── .go-1.24.3.pkg ├── .golangci-lint-1.64.5.pkg ├── .lefthook-1.11.12.pkg ├── README.hermit.md ├── activate-hermit ├── go ├── gofmt ├── golangci-lint ├── hermit ├── hermit.hcl └── lefthook ├── build.go ├── callbacks.go ├── camelcase.go ├── config_test.go ├── context.go ├── defaults.go ├── defaults_test.go ├── doc.go ├── error.go ├── exit.go ├── global.go ├── global_test.go ├── go.mod ├── go.sum ├── guesswidth.go ├── guesswidth_unix.go ├── help.go ├── help_test.go ├── helpwrap1.18_test.go ├── helpwrap1.19_test.go ├── hooks.go ├── interpolate.go ├── interpolate_test.go ├── kong.go ├── kong.png ├── kong.sketch ├── kong_test.go ├── lefthook.yml ├── levenshtein.go ├── mapper.go ├── mapper_linux_test.go ├── mapper_test.go ├── mapper_windows_test.go ├── model.go ├── model_test.go ├── negatable.go ├── options.go ├── options_test.go ├── renovate.json5 ├── resolver.go ├── resolver_test.go ├── scanner.go ├── scanner_test.go ├── tag.go ├── tag_test.go ├── testdata └── file.txt ├── util.go ├── util_test.go └── visit.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [alecthomas] 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | name: CI 7 | 8 | jobs: 9 | test: 10 | name: Test / Go ${{ matrix.go }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | # These are the release channels. 15 | # Hermit will handle installing the right patch. 16 | go: ["1.23", "1.24"] 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | - name: Init Hermit 21 | run: ./bin/hermit env -r >> "$GITHUB_ENV" 22 | - name: Install Go ${{ matrix.go }} 23 | run: | 24 | hermit install go@"${GO_VERSION}" 25 | go version 26 | env: 27 | GO_VERSION: ${{ matrix.go }} 28 | - name: Test 29 | run: go test ./... 30 | 31 | test-windows: 32 | name: Test / Windows / Go ${{ matrix.go }} 33 | runs-on: windows-latest 34 | strategy: 35 | matrix: 36 | # These are versions for GitHub's setup-go. 37 | # '.x' will pick the latest patch release. 38 | go: ["1.23.x", "1.24.x"] 39 | steps: 40 | - name: Checkout code 41 | uses: actions/checkout@v4 42 | - name: Setup Go 43 | uses: actions/setup-go@v5 44 | with: 45 | go-version: ${{ matrix.go }} 46 | - name: Test 47 | run: go test ./... 48 | 49 | lint: 50 | name: Lint 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Checkout code 54 | uses: actions/checkout@v4 55 | - name: Init Hermit 56 | run: ./bin/hermit env -r >> "$GITHUB_ENV" 57 | - name: golangci-lint 58 | run: golangci-lint run 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alecthomas/kong/9bc3bf9925397be48270da0e258bfb0a4f6ed96a/.gitignore -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | tests: true 3 | 4 | output: 5 | print-issued-lines: false 6 | 7 | linters: 8 | enable-all: true 9 | disable: 10 | - lll 11 | - gochecknoglobals 12 | - wsl 13 | - funlen 14 | - gocognit 15 | - goprintffuncname 16 | - paralleltest 17 | - nlreturn 18 | - testpackage 19 | - wrapcheck 20 | - forbidigo 21 | - gci 22 | - godot 23 | - gofumpt 24 | - cyclop 25 | - errorlint 26 | - nestif 27 | - tagliatelle 28 | - thelper 29 | - godox 30 | - goconst 31 | - varnamelen 32 | - ireturn 33 | - exhaustruct 34 | - nonamedreturns 35 | - nilnil 36 | - depguard # nothing to guard against yet 37 | - tagalign # hurts readability of kong tags 38 | - tenv # deprecated since v1.64, but not removed yet 39 | - mnd 40 | - perfsprint 41 | - err113 42 | - copyloopvar 43 | - intrange 44 | - nakedret 45 | - recvcheck # value receivers are intentionally used for copies 46 | 47 | linters-settings: 48 | govet: 49 | # These govet checks are disabled by default, but they're useful. 50 | enable: 51 | - niliness 52 | - sortslice 53 | - unusedwrite 54 | dupl: 55 | threshold: 100 56 | gocyclo: 57 | min-complexity: 20 58 | exhaustive: 59 | default-signifies-exhaustive: true 60 | 61 | issues: 62 | max-per-linter: 0 63 | max-same: 0 64 | exclude-use-default: false 65 | exclude: 66 | - '^(G104|G204):' 67 | # Very commonly not checked. 68 | - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked' 69 | - 'exported method (.*\.MarshalJSON|.*\.UnmarshalJSON) should have comment or be unexported' 70 | - 'composite literal uses unkeyed fields' 71 | - 'bad syntax for struct tag key' 72 | - 'bad syntax for struct tag pair' 73 | - 'result .* \(error\) is always nil' 74 | - 'Error return value of `fmt.Fprintln` is not checked' 75 | 76 | exclude-rules: 77 | # Don't warn on unused parameters. 78 | # Parameter names are useful for documentation. 79 | # Replacing them with '_' hides useful information. 80 | - linters: [revive] 81 | text: 'unused-parameter: parameter \S+ seems to be unused, consider removing or renaming it as _' 82 | 83 | # Duplicate words are okay in tests. 84 | - linters: [dupword] 85 | path: _test\.go 86 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) 2018 Alec Thomas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /_examples/docker/README.md: -------------------------------------------------------------------------------- 1 | # Large-scale composed CLI 2 | 3 | This directory illustrates how a large-scale CLI app could be structured. 4 | -------------------------------------------------------------------------------- /_examples/docker/commands.go: -------------------------------------------------------------------------------- 1 | // nolint 2 | package main 3 | 4 | import "fmt" 5 | 6 | type AttachCmd struct { 7 | DetachKeys string `help:"Override the key sequence for detaching a container"` 8 | NoStdin bool `help:"Do not attach STDIN"` 9 | SigProxy bool `help:"Proxy all received signals to the process" default:"true"` 10 | 11 | Container string `arg required help:"Container ID to attach to."` 12 | } 13 | 14 | func (a *AttachCmd) Run(globals *Globals) error { 15 | fmt.Printf("Config: %s\n", globals.Config) 16 | fmt.Printf("Attaching to: %v\n", a.Container) 17 | fmt.Printf("SigProxy: %v\n", a.SigProxy) 18 | return nil 19 | } 20 | 21 | type BuildCmd struct { 22 | Arg string `arg required` 23 | } 24 | 25 | func (cmd *BuildCmd) Run(globals *Globals) error { 26 | return nil 27 | } 28 | 29 | type CommitCmd struct { 30 | Arg string `arg required` 31 | } 32 | 33 | func (cmd *CommitCmd) Run(globals *Globals) error { 34 | return nil 35 | } 36 | 37 | type CpCmd struct { 38 | Arg string `arg required` 39 | } 40 | 41 | func (cmd *CpCmd) Run(globals *Globals) error { 42 | return nil 43 | } 44 | 45 | type CreateCmd struct { 46 | Arg string `arg required` 47 | } 48 | 49 | func (cmd *CreateCmd) Run(globals *Globals) error { 50 | return nil 51 | } 52 | 53 | type DeployCmd struct { 54 | Arg string `arg required` 55 | } 56 | 57 | func (cmd *DeployCmd) Run(globals *Globals) error { 58 | return nil 59 | } 60 | 61 | type DiffCmd struct { 62 | Arg string `arg required` 63 | } 64 | 65 | func (cmd *DiffCmd) Run(globals *Globals) error { 66 | return nil 67 | } 68 | 69 | type EventsCmd struct { 70 | Arg string `arg required` 71 | } 72 | 73 | func (cmd *EventsCmd) Run(globals *Globals) error { 74 | return nil 75 | } 76 | 77 | type ExecCmd struct { 78 | Arg string `arg required` 79 | } 80 | 81 | func (cmd *ExecCmd) Run(globals *Globals) error { 82 | return nil 83 | } 84 | 85 | type ExportCmd struct { 86 | Arg string `arg required` 87 | } 88 | 89 | func (cmd *ExportCmd) Run(globals *Globals) error { 90 | return nil 91 | } 92 | 93 | type HistoryCmd struct { 94 | Arg string `arg required` 95 | } 96 | 97 | func (cmd *HistoryCmd) Run(globals *Globals) error { 98 | return nil 99 | } 100 | 101 | type ImagesCmd struct { 102 | Arg string `arg required` 103 | } 104 | 105 | func (cmd *ImagesCmd) Run(globals *Globals) error { 106 | return nil 107 | } 108 | 109 | type ImportCmd struct { 110 | Arg string `arg required` 111 | } 112 | 113 | func (cmd *ImportCmd) Run(globals *Globals) error { 114 | return nil 115 | } 116 | 117 | type InfoCmd struct { 118 | Arg string `arg required` 119 | } 120 | 121 | func (cmd *InfoCmd) Run(globals *Globals) error { 122 | return nil 123 | } 124 | 125 | type InspectCmd struct { 126 | Arg string `arg required` 127 | } 128 | 129 | func (cmd *InspectCmd) Run(globals *Globals) error { 130 | return nil 131 | } 132 | 133 | type KillCmd struct { 134 | Arg string `arg required` 135 | } 136 | 137 | func (cmd *KillCmd) Run(globals *Globals) error { 138 | return nil 139 | } 140 | 141 | type LoadCmd struct { 142 | Arg string `arg required` 143 | } 144 | 145 | func (cmd *LoadCmd) Run(globals *Globals) error { 146 | return nil 147 | } 148 | 149 | type LoginCmd struct { 150 | Arg string `arg required` 151 | } 152 | 153 | func (cmd *LoginCmd) Run(globals *Globals) error { 154 | return nil 155 | } 156 | 157 | type LogoutCmd struct { 158 | Arg string `arg required` 159 | } 160 | 161 | func (cmd *LogoutCmd) Run(globals *Globals) error { 162 | return nil 163 | } 164 | 165 | type LogsCmd struct { 166 | Arg string `arg required` 167 | } 168 | 169 | func (cmd *LogsCmd) Run(globals *Globals) error { 170 | return nil 171 | } 172 | 173 | type PauseCmd struct { 174 | Arg string `arg required` 175 | } 176 | 177 | func (cmd *PauseCmd) Run(globals *Globals) error { 178 | return nil 179 | } 180 | 181 | type PortCmd struct { 182 | Arg string `arg required` 183 | } 184 | 185 | func (cmd *PortCmd) Run(globals *Globals) error { 186 | return nil 187 | } 188 | 189 | type PsCmd struct { 190 | Arg string `arg required` 191 | } 192 | 193 | func (cmd *PsCmd) Run(globals *Globals) error { 194 | return nil 195 | } 196 | 197 | type PullCmd struct { 198 | Arg string `arg required` 199 | } 200 | 201 | func (cmd *PullCmd) Run(globals *Globals) error { 202 | return nil 203 | } 204 | 205 | type PushCmd struct { 206 | Arg string `arg required` 207 | } 208 | 209 | func (cmd *PushCmd) Run(globals *Globals) error { 210 | return nil 211 | } 212 | 213 | type RenameCmd struct { 214 | Arg string `arg required` 215 | } 216 | 217 | func (cmd *RenameCmd) Run(globals *Globals) error { 218 | return nil 219 | } 220 | 221 | type RestartCmd struct { 222 | Arg string `arg required` 223 | } 224 | 225 | func (cmd *RestartCmd) Run(globals *Globals) error { 226 | return nil 227 | } 228 | 229 | type RmCmd struct { 230 | Arg string `arg required` 231 | } 232 | 233 | func (cmd *RmCmd) Run(globals *Globals) error { 234 | return nil 235 | } 236 | 237 | type RmiCmd struct { 238 | Arg string `arg required` 239 | } 240 | 241 | func (cmd *RmiCmd) Run(globals *Globals) error { 242 | return nil 243 | } 244 | 245 | type RunCmd struct { 246 | Arg string `arg required` 247 | } 248 | 249 | func (cmd *RunCmd) Run(globals *Globals) error { 250 | return nil 251 | } 252 | 253 | type SaveCmd struct { 254 | Arg string `arg required` 255 | } 256 | 257 | func (cmd *SaveCmd) Run(globals *Globals) error { 258 | return nil 259 | } 260 | 261 | type SearchCmd struct { 262 | Arg string `arg required` 263 | } 264 | 265 | func (cmd *SearchCmd) Run(globals *Globals) error { 266 | return nil 267 | } 268 | 269 | type StartCmd struct { 270 | Arg string `arg required` 271 | } 272 | 273 | func (cmd *StartCmd) Run(globals *Globals) error { 274 | return nil 275 | } 276 | 277 | type StatsCmd struct { 278 | Arg string `arg required` 279 | } 280 | 281 | func (cmd *StatsCmd) Run(globals *Globals) error { 282 | return nil 283 | } 284 | 285 | type StopCmd struct { 286 | Arg string `arg required` 287 | } 288 | 289 | func (cmd *StopCmd) Run(globals *Globals) error { 290 | return nil 291 | } 292 | 293 | type TagCmd struct { 294 | Arg string `arg required` 295 | } 296 | 297 | func (cmd *TagCmd) Run(globals *Globals) error { 298 | return nil 299 | } 300 | 301 | type TopCmd struct { 302 | Arg string `arg required` 303 | } 304 | 305 | func (cmd *TopCmd) Run(globals *Globals) error { 306 | return nil 307 | } 308 | 309 | type UnpauseCmd struct { 310 | Arg string `arg required` 311 | } 312 | 313 | func (cmd *UnpauseCmd) Run(globals *Globals) error { 314 | return nil 315 | } 316 | 317 | type UpdateCmd struct { 318 | Arg string `arg required` 319 | } 320 | 321 | func (cmd *UpdateCmd) Run(globals *Globals) error { 322 | return nil 323 | } 324 | 325 | type VersionCmd struct { 326 | Arg string `arg required` 327 | } 328 | 329 | func (cmd *VersionCmd) Run(globals *Globals) error { 330 | return nil 331 | } 332 | 333 | type WaitCmd struct { 334 | Arg string `arg required` 335 | } 336 | 337 | func (cmd *WaitCmd) Run(globals *Globals) error { 338 | return nil 339 | } 340 | -------------------------------------------------------------------------------- /_examples/docker/main.go: -------------------------------------------------------------------------------- 1 | // nolint 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/alecthomas/kong" 8 | ) 9 | 10 | type Globals struct { 11 | Config string `help:"Location of client config files" default:"~/.docker" type:"path"` 12 | Debug bool `short:"D" help:"Enable debug mode"` 13 | Host []string `short:"H" help:"Daemon socket(s) to connect to"` 14 | LogLevel string `short:"l" help:"Set the logging level (debug|info|warn|error|fatal)" default:"info"` 15 | TLS bool `help:"Use TLS; implied by --tls-verify"` 16 | TLSCACert string `name:"tls-ca-cert" help:"Trust certs signed only by this CA" default:"~/.docker/ca.pem" type:"path"` 17 | TLSCert string `help:"Path to TLS certificate file" default:"~/.docker/cert.pem" type:"path"` 18 | TLSKey string `help:"Path to TLS key file" default:"~/.docker/key.pem" type:"path"` 19 | TLSVerify bool `help:"Use TLS and verify the remote"` 20 | Version VersionFlag `name:"version" help:"Print version information and quit"` 21 | } 22 | 23 | type VersionFlag string 24 | 25 | func (v VersionFlag) Decode(ctx *kong.DecodeContext) error { return nil } 26 | func (v VersionFlag) IsBool() bool { return true } 27 | func (v VersionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error { 28 | fmt.Println(vars["version"]) 29 | app.Exit(0) 30 | return nil 31 | } 32 | 33 | type CLI struct { 34 | Globals 35 | 36 | Attach AttachCmd `cmd:"" help:"Attach local standard input, output, and error streams to a running container"` 37 | Build BuildCmd `cmd:"" help:"Build an image from a Dockerfile"` 38 | Commit CommitCmd `cmd:"" help:"Create a new image from a container's changes"` 39 | Cp CpCmd `cmd:"" help:"Copy files/folders between a container and the local filesystem"` 40 | Create CreateCmd `cmd:"" help:"Create a new container"` 41 | Deploy DeployCmd `cmd:"" help:"Deploy a new stack or update an existing stack"` 42 | Diff DiffCmd `cmd:"" help:"Inspect changes to files or directories on a container's filesystem"` 43 | Events EventsCmd `cmd:"" help:"Get real time events from the server"` 44 | Exec ExecCmd `cmd:"" help:"Run a command in a running container"` 45 | Export ExportCmd `cmd:"" help:"Export a container's filesystem as a tar archive"` 46 | History HistoryCmd `cmd:"" help:"Show the history of an image"` 47 | Images ImagesCmd `cmd:"" help:"List images"` 48 | Import ImportCmd `cmd:"" help:"Import the contents from a tarball to create a filesystem image"` 49 | Info InfoCmd `cmd:"" help:"Display system-wide information"` 50 | Inspect InspectCmd `cmd:"" help:"Return low-level information on Docker objects"` 51 | Kill KillCmd `cmd:"" help:"Kill one or more running containers"` 52 | Load LoadCmd `cmd:"" help:"Load an image from a tar archive or STDIN"` 53 | Login LoginCmd `cmd:"" help:"Log in to a Docker registry"` 54 | Logout LogoutCmd `cmd:"" help:"Log out from a Docker registry"` 55 | Logs LogsCmd `cmd:"" help:"Fetch the logs of a container"` 56 | Pause PauseCmd `cmd:"" help:"Pause all processes within one or more containers"` 57 | Port PortCmd `cmd:"" help:"List port mappings or a specific mapping for the container"` 58 | Ps PsCmd `cmd:"" help:"List containers"` 59 | Pull PullCmd `cmd:"" help:"Pull an image or a repository from a registry"` 60 | Push PushCmd `cmd:"" help:"Push an image or a repository to a registry"` 61 | Rename RenameCmd `cmd:"" help:"Rename a container"` 62 | Restart RestartCmd `cmd:"" help:"Restart one or more containers"` 63 | Rm RmCmd `cmd:"" help:"Remove one or more containers"` 64 | Rmi RmiCmd `cmd:"" help:"Remove one or more images"` 65 | Run RunCmd `cmd:"" help:"Run a command in a new container"` 66 | Save SaveCmd `cmd:"" help:"Save one or more images to a tar archive (streamed to STDOUT by default)"` 67 | Search SearchCmd `cmd:"" help:"Search the Docker Hub for images"` 68 | Start StartCmd `cmd:"" help:"Start one or more stopped containers"` 69 | Stats StatsCmd `cmd:"" help:"Display a live stream of container(s) resource usage statistics"` 70 | Stop StopCmd `cmd:"" help:"Stop one or more running containers"` 71 | Tag TagCmd `cmd:"" help:"Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE"` 72 | Top TopCmd `cmd:"" help:"Display the running processes of a container"` 73 | Unpause UnpauseCmd `cmd:"" help:"Unpause all processes within one or more containers"` 74 | Update UpdateCmd `cmd:"" help:"Update configuration of one or more containers"` 75 | Version VersionCmd `cmd:"" help:"Show the Docker version information"` 76 | Wait WaitCmd `cmd:"" help:"Block until one or more containers stop, then print their exit codes"` 77 | } 78 | 79 | func main() { 80 | cli := CLI{ 81 | Globals: Globals{ 82 | Version: VersionFlag("0.1.1"), 83 | }, 84 | } 85 | 86 | ctx := kong.Parse(&cli, 87 | kong.Name("docker"), 88 | kong.Description("A self-sufficient runtime for containers"), 89 | kong.UsageOnError(), 90 | kong.ConfigureHelp(kong.HelpOptions{ 91 | Compact: true, 92 | }), 93 | kong.Vars{ 94 | "version": "0.0.1", 95 | }) 96 | err := ctx.Run(&cli.Globals) 97 | ctx.FatalIfErrorf(err) 98 | } 99 | -------------------------------------------------------------------------------- /_examples/server/README.md: -------------------------------------------------------------------------------- 1 | # An interactive SSH server 2 | 3 | In addition to command-lines, Kong can be used interactively. This example 4 | serves a Kong command-line over SSH. 5 | 6 | Run with `go run .` then ssh to it like so: 7 | 8 | ``` 9 | $ ssh -p 6740 127.0.0.1 10 | Welcome! 11 | > ? 12 | 13 | Example using Kong for interactive command parsing. 14 | 15 | Commands: 16 | help [ ...] 17 | Show help. 18 | 19 | status 20 | Show server status. 21 | 22 | > status 23 | OK 24 | > help status 25 | 26 | Show server status. 27 | 28 | Flags: 29 | -v, --verbose Show verbose status information. 30 | 31 | > status 32 | OK 33 | > status -v 34 | OK 35 | > status foo 36 | error: unexpected argument foo 37 | 38 | Show server status. 39 | 40 | Flags: 41 | > ^D 42 | ``` 43 | -------------------------------------------------------------------------------- /_examples/server/console.go: -------------------------------------------------------------------------------- 1 | // nolint: govet 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | 7 | "github.com/alecthomas/kong" 8 | ) 9 | 10 | // Ensure the grammar compiles. 11 | var _ = kong.Must(&grammar{}) 12 | 13 | // Server interface. 14 | type grammar struct { 15 | Help helpCmd `cmd:"" help:"Show help."` 16 | Question helpCmd `cmd:"" hidden:"" name:"?" help:"Show help."` 17 | 18 | Status statusCmd `cmd:"" help:"Show server status."` 19 | } 20 | 21 | type statusCmd struct { 22 | Verbose bool `short:"v" help:"Show verbose status information."` 23 | } 24 | 25 | func (s *statusCmd) Run(ctx *kong.Context) error { 26 | ctx.Printf("OK") 27 | return nil 28 | } 29 | 30 | type helpCmd struct { 31 | Command []string `arg:"" optional:"" help:"Show help on command."` 32 | } 33 | 34 | // Run shows help. 35 | func (h *helpCmd) Run(realCtx *kong.Context) error { 36 | ctx, err := kong.Trace(realCtx.Kong, h.Command) 37 | if err != nil { 38 | return err 39 | } 40 | if ctx.Error != nil { 41 | return ctx.Error 42 | } 43 | err = ctx.PrintUsage(false) 44 | if err != nil { 45 | return err 46 | } 47 | fmt.Fprintln(realCtx.Stdout) 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /_examples/server/go.mod: -------------------------------------------------------------------------------- 1 | module kong_server 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/alecthomas/colour v0.1.0 9 | github.com/alecthomas/kong v1.10.0 10 | github.com/chzyer/readline v1.5.1 11 | github.com/gliderlabs/ssh v0.3.8 12 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 13 | github.com/kr/pty v1.1.8 14 | golang.org/x/crypto v0.38.0 15 | ) 16 | 17 | require ( 18 | github.com/alecthomas/assert/v2 v2.11.0 // indirect 19 | github.com/alecthomas/repr v0.4.0 // indirect 20 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect 21 | github.com/chzyer/logex v1.2.1 // indirect 22 | github.com/chzyer/test v1.0.0 // indirect 23 | github.com/creack/pty v1.1.7 // indirect 24 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect 25 | github.com/google/go-cmp v0.6.0 // indirect 26 | github.com/hexops/gotextdiff v1.0.3 // indirect 27 | github.com/mattn/go-isatty v0.0.12 // indirect 28 | github.com/yuin/goldmark v1.4.13 // indirect 29 | golang.org/x/mod v0.17.0 // indirect 30 | golang.org/x/net v0.25.0 // indirect 31 | golang.org/x/sync v0.14.0 // indirect 32 | golang.org/x/sys v0.33.0 // indirect 33 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 // indirect 34 | golang.org/x/term v0.32.0 // indirect 35 | golang.org/x/text v0.25.0 // indirect 36 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 37 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /_examples/server/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.1.0/go.mod h1:b/+1DI2Q6NckYi+3mXyH3wFb8qG37K/DuK80n7WefXA= 2 | github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 3 | github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 4 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 5 | github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk= 6 | github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= 7 | github.com/alecthomas/kong v0.2.1 h1:V1tLBhyQBC4rsbXbcOvm3GBaytJSwRNX69fp1WJxbqQ= 8 | github.com/alecthomas/kong v0.2.1/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= 9 | github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= 10 | github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= 11 | github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= 12 | github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= 13 | github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= 14 | github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= 15 | github.com/alecthomas/kong v1.3.0 h1:YJKuU6/TV2XOBtymafSeuzDvLAFR8cYMZiXVNLhAO6g= 16 | github.com/alecthomas/kong v1.3.0/go.mod h1:IDc8HyiouDdpdiEiY81iaEJM8rSIW6LzX8On4FCO0bE= 17 | github.com/alecthomas/kong v1.4.0 h1:UL7tzGMnnY0YRMMvJyITIRX1EpO6RbBRZDNcCevy3HA= 18 | github.com/alecthomas/kong v1.4.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 19 | github.com/alecthomas/kong v1.5.1 h1:9quB93P2aNGXf5C1kWNei85vjBgITNJQA4dSwJQGCOY= 20 | github.com/alecthomas/kong v1.5.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 21 | github.com/alecthomas/kong v1.6.0 h1:mwOzbdMR7uv2vul9J0FU3GYxE7ls/iX1ieMg5WIM6gE= 22 | github.com/alecthomas/kong v1.6.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 23 | github.com/alecthomas/kong v1.6.1 h1:/7bVimARU3uxPD0hbryPE8qWrS3Oz3kPQoxA/H2NKG8= 24 | github.com/alecthomas/kong v1.6.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 25 | github.com/alecthomas/kong v1.7.0 h1:MnT8+5JxFDCvISeI6vgd/mFbAJwueJ/pqQNzZMsiqZE= 26 | github.com/alecthomas/kong v1.7.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 27 | github.com/alecthomas/kong v1.8.1 h1:6aamvWBE/REnR/BCq10EcozmcpUPc5aGI1lPAWdB0EE= 28 | github.com/alecthomas/kong v1.8.1/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 29 | github.com/alecthomas/kong v1.9.0 h1:Wgg0ll5Ys7xDnpgYBuBn/wPeLGAuK0NvYmEcisJgrIs= 30 | github.com/alecthomas/kong v1.9.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 31 | github.com/alecthomas/kong v1.10.0 h1:8K4rGDpT7Iu+jEXCIJUeKqvpwZHbsFRoebLbnzlmrpw= 32 | github.com/alecthomas/kong v1.10.0/go.mod h1:p2vqieVMeTAnaC83txKtXe8FLke2X07aruPWXyMPQrU= 33 | github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= 34 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 35 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= 36 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 37 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 38 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 39 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= 40 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= 41 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 42 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 43 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= 44 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= 45 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= 46 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= 47 | github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= 48 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 49 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 50 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 51 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= 52 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 53 | github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= 54 | github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 55 | github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= 56 | github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= 57 | github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8= 58 | github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 59 | github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= 60 | github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 61 | github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 62 | github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 63 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 64 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 65 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 66 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 67 | github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= 68 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 69 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 70 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 71 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 72 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 73 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 74 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 75 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 76 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 77 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 78 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 79 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 80 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 81 | golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 82 | golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= 83 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 84 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 85 | golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= 86 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 87 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 88 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 89 | golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= 90 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 91 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 92 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 93 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 94 | golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= 95 | golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= 96 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 97 | golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= 98 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 99 | golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= 100 | golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= 101 | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= 102 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 103 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 104 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 105 | golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= 106 | golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= 107 | golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= 108 | golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 109 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 110 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 111 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 112 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 113 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= 114 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 115 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 116 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 117 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 118 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 119 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 120 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 121 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 122 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 123 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 124 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 125 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 126 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 127 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 128 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 129 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 130 | golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= 131 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 132 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 133 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 134 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 135 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 136 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 137 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 141 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 142 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 143 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 144 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 145 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 146 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 147 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 148 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 149 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 150 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 151 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 153 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 154 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 155 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 156 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 157 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 158 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 159 | golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 160 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 161 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 162 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 163 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 164 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 165 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 166 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 167 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 168 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 169 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 170 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 171 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 172 | golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= 173 | golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 174 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 175 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 176 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 177 | golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= 178 | golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 179 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 180 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 181 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 182 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 183 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 184 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 185 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 186 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 187 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 188 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 189 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 190 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 191 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 192 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 193 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 194 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 195 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 196 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 197 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 198 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 199 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 200 | golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 201 | golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= 202 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 203 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 204 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 205 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 206 | golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= 207 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 208 | golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= 209 | golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= 210 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 211 | golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= 212 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 213 | golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= 214 | golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= 215 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 216 | golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= 217 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 218 | golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= 219 | golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 220 | golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= 221 | golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= 222 | golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= 223 | golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= 224 | golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= 225 | golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= 226 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= 227 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 228 | golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= 229 | golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= 230 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 231 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 232 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 233 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 234 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 235 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 236 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 237 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 238 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 239 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 240 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 241 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 242 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 243 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 244 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 245 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 246 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 247 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 248 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 249 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 250 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 251 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 252 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 253 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 254 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 255 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 256 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 257 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 258 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 259 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 260 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 261 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 262 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 263 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 264 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 265 | -------------------------------------------------------------------------------- /_examples/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "strings" 10 | 11 | "github.com/chzyer/readline" 12 | "github.com/gliderlabs/ssh" 13 | "github.com/google/shlex" 14 | "github.com/kr/pty" 15 | "golang.org/x/crypto/ssh/terminal" 16 | 17 | "github.com/alecthomas/colour" 18 | "github.com/alecthomas/kong" 19 | ) 20 | 21 | // Handle a single SSH interactive connection. 22 | func handle(log *log.Logger, s ssh.Session) error { 23 | log.Printf("New SSH") 24 | sshPty, _, isPty := s.Pty() 25 | if !isPty { 26 | return errors.New("no PTY requested") 27 | } 28 | log.Printf("Using TERM=%s width=%d height=%d", sshPty.Term, sshPty.Window.Width, sshPty.Window.Height) 29 | cpty, tty, err := pty.Open() 30 | if err != nil { 31 | return err 32 | } 33 | defer tty.Close() 34 | state, err := terminal.GetState(int(cpty.Fd())) 35 | if err != nil { 36 | return err 37 | } 38 | defer terminal.Restore(int(cpty.Fd()), state) 39 | 40 | colour.Fprintln(tty, "^BWelcome!^R") 41 | go io.Copy(cpty, s) 42 | go io.Copy(s, cpty) 43 | 44 | parser, err := buildShellParser(tty) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | rl, err := readline.NewEx(&readline.Config{ 50 | Prompt: "> ", 51 | Stderr: tty, 52 | Stdout: tty, 53 | Stdin: tty, 54 | FuncOnWidthChanged: func(f func()) {}, 55 | FuncMakeRaw: func() error { 56 | _, err := terminal.MakeRaw(int(cpty.Fd())) // nolint: govet 57 | return err 58 | }, 59 | FuncExitRaw: func() error { return nil }, 60 | }) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | log.Printf("Loop") 66 | for { 67 | tty.Sync() 68 | 69 | var line string 70 | line, err = rl.Readline() 71 | if err != nil { 72 | if err == io.EOF { 73 | return nil 74 | } 75 | return err 76 | } 77 | var args []string 78 | args, err := shlex.Split(string(line)) 79 | if err != nil { 80 | parser.Errorf("%s", err) 81 | continue 82 | } 83 | var ctx *kong.Context 84 | ctx, err = parser.Parse(args) 85 | if err != nil { 86 | parser.Errorf("%s", err) 87 | if err, ok := err.(*kong.ParseError); ok { 88 | log.Println(err.Error()) 89 | err.Context.PrintUsage(false) 90 | } 91 | continue 92 | } 93 | err = ctx.Run(ctx) 94 | if err != nil { 95 | parser.Errorf("%s", err) 96 | continue 97 | } 98 | } 99 | } 100 | 101 | func buildShellParser(tty *os.File) (*kong.Kong, error) { 102 | parser, err := kong.New(&grammar{}, 103 | kong.Name(""), 104 | kong.Description("Example using Kong for interactive command parsing."), 105 | kong.Writers(tty, tty), 106 | kong.Exit(func(int) {}), 107 | kong.ConfigureHelp(kong.HelpOptions{ 108 | NoAppSummary: true, 109 | }), 110 | kong.NoDefaultHelp(), 111 | ) 112 | return parser, err 113 | } 114 | 115 | func handlerWithError(handle func(log *log.Logger, s ssh.Session) error) ssh.Handler { 116 | return func(s ssh.Session) { 117 | prefix := fmt.Sprintf("%s->%s ", s.LocalAddr(), s.RemoteAddr()) 118 | l := log.New(os.Stdout, prefix, log.LstdFlags) 119 | err := handle(l, s) 120 | if err != nil { 121 | log.Printf("error: %s", err) 122 | s.Exit(1) 123 | } else { 124 | log.Printf("Bye") 125 | s.Exit(0) 126 | } 127 | } 128 | } 129 | 130 | var cli struct { 131 | HostKey string `type:"existingfile" help:"SSH host key to use." default:"server_rsa_key"` 132 | Bind string `help:"Bind address for server." default:"127.0.0.1:6740"` 133 | } 134 | 135 | func main() { 136 | ctx := kong.Parse(&cli, 137 | kong.Name("server"), 138 | kong.Description("A network server using Kong for interacting with clients.")) 139 | 140 | ssh.Handle(handlerWithError(handle)) 141 | log.Printf("SSH listening on: %s", cli.Bind) 142 | log.Printf("Using host key: %s", cli.HostKey) 143 | log.Println() 144 | parts := strings.Split(cli.Bind, ":") 145 | log.Printf("Connect with: ssh -p %s %s", parts[1], parts[0]) 146 | log.Println() 147 | err := ssh.ListenAndServe(cli.Bind, nil, ssh.HostKeyFile(cli.HostKey)) 148 | ctx.FatalIfErrorf(err) 149 | } 150 | -------------------------------------------------------------------------------- /_examples/server/server_rsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAxK1QbPQYibc0VFZdc6GHPka5oTeGnXsMZiyX4JbHUJl1oNDB 3 | Xg9NATbft+q/6ZDjyVEQhq8xgLvYFkL8qBLt/6UAaOub0RtmPqQwmxoNLWuXFFwn 4 | YlBKApQ4gf58/jOcGPpdEwfjkjwLb536Bni25XMU4cYrdIvQIhtMaK+Zqja/3MAC 5 | V6ZRZZCd8hABJqaZu+3mRElnF1d7gfMvA/hhaq7Y5VYr8rUrBHHimrT/GEP6aCbf 6 | Npo43SfRnUDu2+EAK7vA9cM8fg/O/mvNR1/9jzOWyr8ZDLD6R6iaZCQ2anEPcqim 7 | MCOtibSeVOms07Zcn/TSsgmfGwa8rQkpXVJA5wIDAQABAoIBAQCTUccGdajTrzkx 8 | WyfQ71NgoJV3XyIkYAEfn5N8FTTi+LAVb4kILanemP3mw55RE8isCV65pA0OgqYP 9 | tsmOE+/WKAAwlxs1/LIPhekqpM7uEMMv6v9NMxrc562UIc36kyn/w7loAebCqNtg 10 | FhMsOcu1/wfLPidau0eB5LTNTYtq5RuSKxoindvatk+Zmk0KjoA+f25MlwCEHQNr 11 | ygpopclyTHVln2t3t0o97/a7dHa9+HlmVO4GxWvTTiqtcFErTGWtTUW8aeZFS83r 12 | p+JZNxReSJ2MlM9bm15wJ0L86GTeYZQiaNuC1XETbFvX+9Ffkl+7EtsdYDLV1N6r 13 | /eOP2f0hAoGBAOKVDHmnru7SVmbH5BI8HW5sd6IVztZM3+BKzy6AaPc4/FgG6MOr 14 | bJyFbmN8S/gVi4OYOJXgfaeKcycYJFTjXUSnNRQ9eT0MseD9SxzEXV7RGtnvudiu 15 | pbRmtBRtf3e4beaN9X4SfWk4+Frw7B8UsPXwV/09s7AW279cES565IkfAoGBAN42 16 | TQSC/jQmJBpGSnqWfqQtKPTSKFoZ/JQbxoy9QckAMqVSFwBBgwQYr4MbI7WyjPRE 17 | s43kpf+Sq/++fc3hyk5XAWBKscK0KLs0HBRZyLybQYI+f4/x2giVzKeRRNVa9nQa 18 | VdIU8i+eO2AUzG690q89HGkRBsfXekjq5kXC9Cc5AoGAUY0b5F16FPMXrf6cFAQX 19 | A7t+g5Qd0fvxSCUk1LPbE8Aq8vPpqyN0ABH2XVBLd4spn7+V/jvCfh7Su2txCCyd 20 | USxtak+F53c+PqBr/HqgsJPKek5SMa8KbRfaENAoZMq4o5bMmQfGo6yhlvnHwpgL 21 | 6TkMMlWW6vYPOZzFglkxEDkCgYApT78Rz6ii2VRs7hR6pe/1Zc/vdAK8fYhPoLpQ 22 | //5y9+5yfch467UH1e8LWMhSx1cdMoiPIKsb0JDZgviwhgGufs5qsHhL0mKgKxft 23 | UKPZLKQJKsVcZYI7hl396Sv63mZjP2IlJG/CGpC/VB6NmAzLN3lIrzmrfYvmcoVN 24 | AumRQQKBgB4Uznek3nq5ZQf93+ucvXpf0bLRqq1va7jYmRosyiCN52grbclj5uMq 25 | vxr1uoqmgtCfqdgUbm0s+kVK6D4bPkz4HQOSXImXhLs8/KdixYfPLSarxYvTxZKg 26 | mMF1XqcdRwSv3RZYtUbbF7dYQYsC1/ZKXvtPldeoDmTZ+U7b2qbE 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /_examples/server/server_rsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErVBs9BiJtzRUVl1zoYc+RrmhN4adewxmLJfglsdQmXWg0MFeD00BNt+36r/pkOPJURCGrzGAu9gWQvyoEu3/pQBo65vRG2Y+pDCbGg0ta5cUXCdiUEoClDiB/nz+M5wY+l0TB+OSPAtvnfoGeLblcxThxit0i9AiG0xor5mqNr/cwAJXplFlkJ3yEAEmppm77eZESWcXV3uB8y8D+GFqrtjlVivytSsEceKatP8YQ/poJt82mjjdJ9GdQO7b4QAru8D1wzx+D87+a81HX/2PM5bKvxkMsPpHqJpkJDZqcQ9yqKYwI62JtJ5U6azTtlyf9NKyCZ8bBrytCSldUkDn 2 | -------------------------------------------------------------------------------- /_examples/shell/commandstring/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/alecthomas/kong" 6 | ) 7 | 8 | var cli struct { 9 | Debug bool `help:"Debug mode."` 10 | 11 | Rm struct { 12 | User string `help:"Run as user." short:"u" default:"default"` 13 | Force bool `help:"Force removal." short:"f"` 14 | Recursive bool `help:"Recursively remove files." short:"r"` 15 | 16 | Paths []string `arg:"" help:"Paths to remove." type:"path" name:"path"` 17 | } `cmd:"" help:"Remove files."` 18 | 19 | Ls struct { 20 | Paths []string `arg:"" optional:"" help:"Paths to list." type:"path"` 21 | } `cmd:"" help:"List paths."` 22 | } 23 | 24 | func main() { 25 | ctx := kong.Parse(&cli, 26 | kong.Name("shell"), 27 | kong.Description("A shell-like example app."), 28 | kong.UsageOnError(), 29 | kong.ConfigureHelp(kong.HelpOptions{ 30 | Compact: true, 31 | Summary: true, 32 | })) 33 | switch ctx.Command() { 34 | case "rm ": 35 | fmt.Println(cli.Rm.Paths, cli.Rm.Force, cli.Rm.Recursive) 36 | 37 | case "ls": 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /_examples/shell/help/README.md: -------------------------------------------------------------------------------- 1 | # Example of custom help providers 2 | 3 | This example demonstrates how to add `Help() string` functions (ie. the `HelpProvider` interface) to your commands, arguments, and flags to augment the help text provided using `help:""` style tagged annotations. 4 | -------------------------------------------------------------------------------- /_examples/shell/help/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/alecthomas/kong" 7 | ) 8 | 9 | var cli struct { 10 | Flag flagWithHelp `help:"${flag_help}"` 11 | Echo commandWithHelp `cmd:"" help:"Regular command help"` 12 | } 13 | 14 | type flagWithHelp bool 15 | 16 | // See https://github.com/alecthomas/kong?tab=readme-ov-file#variable-interpolation 17 | var vars = kong.Vars{ 18 | "flag_help": "Extended flag help that might be too long for directly " + 19 | "including in the struct tag field", 20 | } 21 | 22 | type commandWithHelp struct { 23 | Msg argumentWithHelp `arg:"" help:"Regular argument help"` 24 | } 25 | 26 | func (c *commandWithHelp) Help() string { 27 | return "🚀 additional command help" 28 | } 29 | 30 | type argumentWithHelp struct { 31 | Msg string `arg:""` 32 | } 33 | 34 | func (f *argumentWithHelp) Help() string { 35 | return "📣 additional argument help" 36 | } 37 | 38 | func main() { 39 | ctx := kong.Parse(&cli, 40 | kong.Name("help"), 41 | kong.Description("An app demonstrating HelpProviders"), 42 | kong.UsageOnError(), 43 | kong.ConfigureHelp(kong.HelpOptions{ 44 | Compact: true, 45 | Summary: false, 46 | }), 47 | vars) 48 | switch ctx.Command() { 49 | case "echo ": 50 | fmt.Println(cli.Echo.Msg) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | ) 10 | 11 | func BenchmarkKong_interpolate(b *testing.B) { 12 | prepareKong := func(t testing.TB, count int) *Kong { 13 | t.Helper() 14 | k := &Kong{ 15 | vars: make(Vars, count), 16 | registry: NewRegistry().RegisterDefaults(), 17 | } 18 | for i := 0; i < count; i++ { 19 | helpVar := fmt.Sprintf("help_param%d", i) 20 | k.vars[helpVar] = strconv.Itoa(i) 21 | } 22 | grammar := &struct { 23 | Param0 string `help:"${help_param0}"` 24 | }{} 25 | model, err := build(k, grammar) 26 | assert.NoError(t, err) 27 | for i := 0; i < count; i++ { 28 | model.Node.Flags = append(model.Node.Flags, &Flag{ 29 | Value: &Value{ 30 | Help: fmt.Sprintf("${help_param%d}", i), 31 | Tag: newEmptyTag(), 32 | }, 33 | }) 34 | } 35 | k.Model = model 36 | return k 37 | } 38 | 39 | for _, count := range []int{5, 500, 5000} { 40 | count := count 41 | b.Run(strconv.Itoa(count), func(b *testing.B) { 42 | var err error 43 | k := prepareKong(b, count) 44 | for i := 0; i < b.N; i++ { 45 | err = k.interpolate(k.Model.Node) 46 | } 47 | assert.NoError(b, err) 48 | b.ReportAllocs() 49 | }) 50 | } 51 | } 52 | 53 | func Benchmark_interpolateValue(b *testing.B) { 54 | varsLen := 10000 55 | k := &Kong{ 56 | vars: make(Vars, 10000), 57 | registry: NewRegistry().RegisterDefaults(), 58 | } 59 | for i := 0; i < varsLen; i++ { 60 | helpVar := fmt.Sprintf("help_param%d", i) 61 | k.vars[helpVar] = strconv.Itoa(i) 62 | } 63 | grammar := struct { 64 | Param9999 string `kong:"cmd,help=${help_param9999}"` 65 | }{} 66 | model, err := build(k, &grammar) 67 | if err != nil { 68 | b.FailNow() 69 | } 70 | k.Model = model 71 | flag := k.Model.Flags[0] 72 | for i := 0; i < b.N; i++ { 73 | err = k.interpolateValue(flag.Value, k.vars) 74 | if err != nil { 75 | b.FailNow() 76 | } 77 | } 78 | b.ReportAllocs() 79 | } 80 | -------------------------------------------------------------------------------- /bin/.go-1.24.3.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/.golangci-lint-1.64.5.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/.lefthook-1.11.12.pkg: -------------------------------------------------------------------------------- 1 | hermit -------------------------------------------------------------------------------- /bin/README.hermit.md: -------------------------------------------------------------------------------- 1 | # Hermit environment 2 | 3 | This is a [Hermit](https://github.com/cashapp/hermit) bin directory. 4 | 5 | The symlinks in this directory are managed by Hermit and will automatically 6 | download and install Hermit itself as well as packages. These packages are 7 | local to this environment. 8 | -------------------------------------------------------------------------------- /bin/activate-hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file must be used with "source bin/activate-hermit" from bash or zsh. 3 | # You cannot run it directly 4 | 5 | if [ "${BASH_SOURCE-}" = "$0" ]; then 6 | echo "You must source this script: \$ source $0" >&2 7 | exit 33 8 | fi 9 | 10 | BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" 11 | if "${BIN_DIR}/hermit" noop > /dev/null; then 12 | eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")" 13 | 14 | if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then 15 | hash -r 2>/dev/null 16 | fi 17 | 18 | echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated" 19 | fi 20 | -------------------------------------------------------------------------------- /bin/go: -------------------------------------------------------------------------------- 1 | .go-1.24.3.pkg -------------------------------------------------------------------------------- /bin/gofmt: -------------------------------------------------------------------------------- 1 | .go-1.24.3.pkg -------------------------------------------------------------------------------- /bin/golangci-lint: -------------------------------------------------------------------------------- 1 | .golangci-lint-1.64.5.pkg -------------------------------------------------------------------------------- /bin/hermit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eo pipefail 4 | 5 | if [ -z "${HERMIT_STATE_DIR}" ]; then 6 | case "$(uname -s)" in 7 | Darwin) 8 | export HERMIT_STATE_DIR="${HOME}/Library/Caches/hermit" 9 | ;; 10 | Linux) 11 | export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/hermit" 12 | ;; 13 | esac 14 | fi 15 | 16 | export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" 17 | HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" 18 | export HERMIT_CHANNEL 19 | export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} 20 | 21 | if [ ! -x "${HERMIT_EXE}" ]; then 22 | echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 23 | curl -fsSL "${HERMIT_DIST_URL}/install.sh" | /bin/bash 1>&2 24 | fi 25 | 26 | exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@" 27 | -------------------------------------------------------------------------------- /bin/hermit.hcl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alecthomas/kong/9bc3bf9925397be48270da0e258bfb0a4f6ed96a/bin/hermit.hcl -------------------------------------------------------------------------------- /bin/lefthook: -------------------------------------------------------------------------------- 1 | .lefthook-1.11.12.pkg -------------------------------------------------------------------------------- /build.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // Plugins are dynamically embedded command-line structures. 10 | // 11 | // Each element in the Plugins list *must* be a pointer to a structure. 12 | type Plugins []any 13 | 14 | func build(k *Kong, ast any) (app *Application, err error) { 15 | v := reflect.ValueOf(ast) 16 | iv := reflect.Indirect(v) 17 | if v.Kind() != reflect.Ptr || iv.Kind() != reflect.Struct { 18 | return nil, fmt.Errorf("expected a pointer to a struct but got %T", ast) 19 | } 20 | 21 | app = &Application{} 22 | extraFlags := k.extraFlags() 23 | seenFlags := map[string]bool{} 24 | for _, flag := range extraFlags { 25 | seenFlags[flag.Name] = true 26 | } 27 | 28 | node, err := buildNode(k, iv, ApplicationNode, newEmptyTag(), seenFlags) 29 | if err != nil { 30 | return nil, err 31 | } 32 | if len(node.Positional) > 0 && len(node.Children) > 0 { 33 | return nil, fmt.Errorf("can't mix positional arguments and branching arguments on %T", ast) 34 | } 35 | app.Node = node 36 | app.Node.Flags = append(extraFlags, app.Node.Flags...) 37 | app.Tag = newEmptyTag() 38 | app.Tag.Vars = k.vars 39 | return app, nil 40 | } 41 | 42 | func dashedString(s string) string { 43 | return strings.Join(camelCase(s), "-") 44 | } 45 | 46 | type flattenedField struct { 47 | field reflect.StructField 48 | value reflect.Value 49 | tag *Tag 50 | } 51 | 52 | func flattenedFields(v reflect.Value, ptag *Tag) (out []flattenedField, err error) { 53 | v = reflect.Indirect(v) 54 | if v.Kind() != reflect.Struct { 55 | return out, nil 56 | } 57 | ignored := map[string]bool{} 58 | for i := 0; i < v.NumField(); i++ { 59 | ft := v.Type().Field(i) 60 | fv := v.Field(i) 61 | tag, err := parseTag(v, ft) 62 | if err != nil { 63 | return nil, err 64 | } 65 | if tag.Ignored || ignored[ft.Name] { 66 | ignored[ft.Name] = true 67 | continue 68 | } 69 | // Assign group if it's not already set. 70 | if tag.Group == "" { 71 | tag.Group = ptag.Group 72 | } 73 | // Accumulate prefixes. 74 | tag.Prefix = ptag.Prefix + tag.Prefix 75 | tag.EnvPrefix = ptag.EnvPrefix + tag.EnvPrefix 76 | tag.XorPrefix = ptag.XorPrefix + tag.XorPrefix 77 | // Combine parent vars. 78 | tag.Vars = ptag.Vars.CloneWith(tag.Vars) 79 | // Command and embedded structs can be pointers, so we hydrate them now. 80 | if (tag.Cmd || tag.Embed) && ft.Type.Kind() == reflect.Ptr { 81 | fv = reflect.New(ft.Type.Elem()).Elem() 82 | v.FieldByIndex(ft.Index).Set(fv.Addr()) 83 | } 84 | if !ft.Anonymous && !tag.Embed { 85 | if fv.CanSet() { 86 | field := flattenedField{field: ft, value: fv, tag: tag} 87 | out = append(out, field) 88 | } 89 | continue 90 | } 91 | 92 | // Embedded type. 93 | if fv.Kind() == reflect.Interface { 94 | fv = fv.Elem() 95 | } else if fv.Type() == reflect.TypeOf(Plugins{}) { 96 | for i := 0; i < fv.Len(); i++ { 97 | fields, ferr := flattenedFields(fv.Index(i).Elem(), tag) 98 | if ferr != nil { 99 | return nil, ferr 100 | } 101 | out = append(out, fields...) 102 | } 103 | continue 104 | } 105 | sub, err := flattenedFields(fv, tag) 106 | if err != nil { 107 | return nil, err 108 | } 109 | out = append(out, sub...) 110 | } 111 | out = removeIgnored(out, ignored) 112 | return out, nil 113 | } 114 | 115 | func removeIgnored(fields []flattenedField, ignored map[string]bool) []flattenedField { 116 | j := 0 117 | for i := 0; i < len(fields); i++ { 118 | if ignored[fields[i].field.Name] { 119 | continue 120 | } 121 | if i != j { 122 | fields[j] = fields[i] 123 | } 124 | j++ 125 | } 126 | if j != len(fields) { 127 | fields = fields[:j] 128 | } 129 | return fields 130 | } 131 | 132 | // Build a Node in the Kong data model. 133 | // 134 | // "v" is the value to create the node from, "typ" is the output Node type. 135 | func buildNode(k *Kong, v reflect.Value, typ NodeType, tag *Tag, seenFlags map[string]bool) (*Node, error) { //nolint:gocyclo 136 | node := &Node{ 137 | Type: typ, 138 | Target: v, 139 | Tag: tag, 140 | } 141 | fields, err := flattenedFields(v, tag) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | MAIN: 147 | for _, field := range fields { 148 | for _, r := range k.ignoreFields { 149 | if r.MatchString(v.Type().Name() + "." + field.field.Name) { 150 | continue MAIN 151 | } 152 | } 153 | 154 | ft := field.field 155 | fv := field.value 156 | 157 | tag := field.tag 158 | name := tag.Name 159 | if name == "" { 160 | name = tag.Prefix + k.flagNamer(ft.Name) 161 | } else { 162 | name = tag.Prefix + name 163 | } 164 | 165 | if len(tag.Envs) != 0 { 166 | for i := range tag.Envs { 167 | tag.Envs[i] = tag.EnvPrefix + tag.Envs[i] 168 | } 169 | } 170 | 171 | if len(tag.Xor) != 0 { 172 | for i := range tag.Xor { 173 | tag.Xor[i] = tag.XorPrefix + tag.Xor[i] 174 | } 175 | } 176 | 177 | if len(tag.And) != 0 { 178 | for i := range tag.And { 179 | tag.And[i] = tag.XorPrefix + tag.And[i] 180 | } 181 | } 182 | 183 | // Nested structs are either commands or args, unless they implement the Mapper interface. 184 | if field.value.Kind() == reflect.Struct && (tag.Cmd || tag.Arg) && k.registry.ForValue(fv) == nil { 185 | typ := CommandNode 186 | if tag.Arg { 187 | typ = ArgumentNode 188 | } 189 | err = buildChild(k, node, typ, v, ft, fv, tag, name, seenFlags) 190 | } else { 191 | err = buildField(k, node, v, ft, fv, tag, name, seenFlags) 192 | } 193 | if err != nil { 194 | return nil, err 195 | } 196 | } 197 | 198 | // Validate if there are no duplicate names 199 | if err := checkDuplicateNames(node, v); err != nil { 200 | return nil, err 201 | } 202 | 203 | // "Unsee" flags. 204 | for _, flag := range node.Flags { 205 | delete(seenFlags, "--"+flag.Name) 206 | if flag.Short != 0 { 207 | delete(seenFlags, "-"+string(flag.Short)) 208 | } 209 | if negFlag := negatableFlagName(flag.Name, flag.Tag.Negatable); negFlag != "" { 210 | delete(seenFlags, negFlag) 211 | } 212 | for _, aflag := range flag.Aliases { 213 | delete(seenFlags, "--"+aflag) 214 | } 215 | } 216 | 217 | if err := validatePositionalArguments(node); err != nil { 218 | return nil, err 219 | } 220 | 221 | return node, nil 222 | } 223 | 224 | func validatePositionalArguments(node *Node) error { 225 | var last *Value 226 | for i, curr := range node.Positional { 227 | if last != nil { 228 | // Scan through argument positionals to ensure optional is never before a required. 229 | if !last.Required && curr.Required { 230 | return fmt.Errorf("%s: required %q cannot come after optional %q", node.FullPath(), curr.Name, last.Name) 231 | } 232 | 233 | // Cumulative argument needs to be last. 234 | if last.IsCumulative() { 235 | return fmt.Errorf("%s: argument %q cannot come after cumulative %q", node.FullPath(), curr.Name, last.Name) 236 | } 237 | } 238 | 239 | last = curr 240 | curr.Position = i 241 | } 242 | 243 | return nil 244 | } 245 | 246 | func buildChild(k *Kong, node *Node, typ NodeType, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error { 247 | child, err := buildNode(k, fv, typ, newEmptyTag(), seenFlags) 248 | if err != nil { 249 | return err 250 | } 251 | child.Name = name 252 | child.Tag = tag 253 | child.Parent = node 254 | child.Help = tag.Help 255 | child.Hidden = tag.Hidden 256 | child.Group = buildGroupForKey(k, tag.Group) 257 | child.Aliases = tag.Aliases 258 | 259 | if provider, ok := fv.Addr().Interface().(HelpProvider); ok { 260 | child.Detail = provider.Help() 261 | } 262 | 263 | // A branching argument. This is a bit hairy, as we let buildNode() do the parsing, then check that 264 | // a positional argument is provided to the child, and move it to the branching argument field. 265 | if tag.Arg { 266 | if len(child.Positional) == 0 { 267 | return failField(v, ft, "positional branch must have at least one child positional argument named %q", name) 268 | } 269 | if child.Positional[0].Name != name { 270 | return failField(v, ft, "first field in positional branch must have the same name as the parent field (%s).", child.Name) 271 | } 272 | 273 | child.Argument = child.Positional[0] 274 | child.Positional = child.Positional[1:] 275 | if child.Help == "" { 276 | child.Help = child.Argument.Help 277 | } 278 | } else { 279 | if tag.HasDefault { 280 | if node.DefaultCmd != nil { 281 | return failField(v, ft, "can't have more than one default command under %s", node.Summary()) 282 | } 283 | if tag.Default != "withargs" && (len(child.Children) > 0 || len(child.Positional) > 0) { 284 | return failField(v, ft, "default command %s must not have subcommands or arguments", child.Summary()) 285 | } 286 | node.DefaultCmd = child 287 | } 288 | if tag.Passthrough { 289 | if len(child.Children) > 0 || len(child.Flags) > 0 { 290 | return failField(v, ft, "passthrough command %s must not have subcommands or flags", child.Summary()) 291 | } 292 | if len(child.Positional) != 1 { 293 | return failField(v, ft, "passthrough command %s must contain exactly one positional argument", child.Summary()) 294 | } 295 | if !checkPassthroughArg(child.Positional[0].Target) { 296 | return failField(v, ft, "passthrough command %s must contain exactly one positional argument of []string type", child.Summary()) 297 | } 298 | child.Passthrough = true 299 | } 300 | } 301 | node.Children = append(node.Children, child) 302 | 303 | if len(child.Positional) > 0 && len(child.Children) > 0 { 304 | return failField(v, ft, "can't mix positional arguments and branching arguments") 305 | } 306 | 307 | return nil 308 | } 309 | 310 | func buildField(k *Kong, node *Node, v reflect.Value, ft reflect.StructField, fv reflect.Value, tag *Tag, name string, seenFlags map[string]bool) error { 311 | mapper := k.registry.ForNamedValue(tag.Type, fv) 312 | if mapper == nil { 313 | return failField(v, ft, "unsupported field type %s, perhaps missing a cmd:\"\" tag?", ft.Type) 314 | } 315 | 316 | value := &Value{ 317 | Name: name, 318 | Help: tag.Help, 319 | OrigHelp: tag.Help, 320 | HasDefault: tag.HasDefault, 321 | Default: tag.Default, 322 | DefaultValue: reflect.New(fv.Type()).Elem(), 323 | Mapper: mapper, 324 | Tag: tag, 325 | Target: fv, 326 | Enum: tag.Enum, 327 | Passthrough: tag.Passthrough, 328 | PassthroughMode: tag.PassthroughMode, 329 | 330 | // Flags are optional by default, and args are required by default. 331 | Required: (!tag.Arg && tag.Required) || (tag.Arg && !tag.Optional), 332 | Format: tag.Format, 333 | } 334 | 335 | if tag.Arg { 336 | node.Positional = append(node.Positional, value) 337 | } else { 338 | if seenFlags["--"+value.Name] { 339 | return failField(v, ft, "duplicate flag --%s", value.Name) 340 | } 341 | seenFlags["--"+value.Name] = true 342 | for _, alias := range tag.Aliases { 343 | aliasFlag := "--" + alias 344 | if seenFlags[aliasFlag] { 345 | return failField(v, ft, "duplicate flag %s", aliasFlag) 346 | } 347 | seenFlags[aliasFlag] = true 348 | } 349 | if tag.Short != 0 { 350 | if seenFlags["-"+string(tag.Short)] { 351 | return failField(v, ft, "duplicate short flag -%c", tag.Short) 352 | } 353 | seenFlags["-"+string(tag.Short)] = true 354 | } 355 | if tag.Negatable != "" { 356 | negFlag := negatableFlagName(value.Name, tag.Negatable) 357 | if seenFlags[negFlag] { 358 | return failField(v, ft, "duplicate negation flag %s", negFlag) 359 | } 360 | seenFlags[negFlag] = true 361 | } 362 | flag := &Flag{ 363 | Value: value, 364 | Aliases: tag.Aliases, 365 | Short: tag.Short, 366 | PlaceHolder: tag.PlaceHolder, 367 | Envs: tag.Envs, 368 | Group: buildGroupForKey(k, tag.Group), 369 | Xor: tag.Xor, 370 | And: tag.And, 371 | Hidden: tag.Hidden, 372 | } 373 | value.Flag = flag 374 | node.Flags = append(node.Flags, flag) 375 | } 376 | return nil 377 | } 378 | 379 | func buildGroupForKey(k *Kong, key string) *Group { 380 | if key == "" { 381 | return nil 382 | } 383 | for _, group := range k.groups { 384 | if group.Key == key { 385 | return &group 386 | } 387 | } 388 | 389 | // No group provided with kong.ExplicitGroups. We create one ad-hoc for this key. 390 | return &Group{ 391 | Key: key, 392 | Title: key, 393 | } 394 | } 395 | 396 | func checkDuplicateNames(node *Node, v reflect.Value) error { 397 | seenNames := make(map[string]struct{}) 398 | for _, node := range node.Children { 399 | if _, ok := seenNames[node.Name]; ok { 400 | name := v.Type().Name() 401 | if name == "" { 402 | name = "" 403 | } 404 | return fmt.Errorf("duplicate command name %q in command %q", node.Name, name) 405 | } 406 | 407 | seenNames[node.Name] = struct{}{} 408 | } 409 | 410 | return nil 411 | } 412 | -------------------------------------------------------------------------------- /callbacks.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | // binding is a single binding registered with Kong. 10 | type binding struct { 11 | // fn is a function that returns a value of the target type. 12 | fn reflect.Value 13 | 14 | // val is a value of the target type. 15 | // Must be set if done and singleton are true. 16 | val reflect.Value 17 | 18 | // singleton indicates whether the binding is a singleton. 19 | // If true, the binding will be resolved once and cached. 20 | singleton bool 21 | 22 | // done indicates whether a singleton binding has been resolved. 23 | // If singleton is false, this field is ignored. 24 | done bool 25 | } 26 | 27 | // newValueBinding builds a binding with an already resolved value. 28 | func newValueBinding(v reflect.Value) *binding { 29 | return &binding{val: v, done: true, singleton: true} 30 | } 31 | 32 | // newFunctionBinding builds a binding with a function 33 | // that will return a value of the target type. 34 | // 35 | // The function signature must be func(...) (T, error) or func(...) T 36 | // where parameters are recursively resolved. 37 | func newFunctionBinding(f reflect.Value, singleton bool) *binding { 38 | return &binding{fn: f, singleton: singleton} 39 | } 40 | 41 | // Get returns the pre-resolved value for the binding, 42 | // or false if the binding is not resolved. 43 | func (b *binding) Get() (v reflect.Value, ok bool) { 44 | return b.val, b.done 45 | } 46 | 47 | // Set sets the value of the binding to the given value, 48 | // marking it as resolved. 49 | // 50 | // If the binding is not a singleton, this method does nothing. 51 | func (b *binding) Set(v reflect.Value) { 52 | if b.singleton { 53 | b.val = v 54 | b.done = true 55 | } 56 | } 57 | 58 | // A map of type to function that returns a value of that type. 59 | // 60 | // The function should have the signature func(...) (T, error). Arguments are recursively resolved. 61 | type bindings map[reflect.Type]*binding 62 | 63 | func (b bindings) String() string { 64 | out := []string{} 65 | for k := range b { 66 | out = append(out, k.String()) 67 | } 68 | return "bindings{" + strings.Join(out, ", ") + "}" 69 | } 70 | 71 | func (b bindings) add(values ...any) bindings { 72 | for _, v := range values { 73 | val := reflect.ValueOf(v) 74 | b[val.Type()] = newValueBinding(val) 75 | } 76 | return b 77 | } 78 | 79 | func (b bindings) addTo(impl, iface any) { 80 | val := reflect.ValueOf(impl) 81 | b[reflect.TypeOf(iface).Elem()] = newValueBinding(val) 82 | } 83 | 84 | func (b bindings) addProvider(provider any, singleton bool) error { 85 | pv := reflect.ValueOf(provider) 86 | t := pv.Type() 87 | if t.Kind() != reflect.Func { 88 | return fmt.Errorf("%T must be a function", provider) 89 | } 90 | 91 | if t.NumOut() == 0 { 92 | return fmt.Errorf("%T must be a function with the signature func(...)(T, error) or func(...) T", provider) 93 | } 94 | if t.NumOut() == 2 { 95 | if t.Out(1) != reflect.TypeOf((*error)(nil)).Elem() { 96 | return fmt.Errorf("missing error; %T must be a function with the signature func(...)(T, error) or func(...) T", provider) 97 | } 98 | } 99 | rt := pv.Type().Out(0) 100 | b[rt] = newFunctionBinding(pv, singleton) 101 | return nil 102 | } 103 | 104 | // Clone and add values. 105 | func (b bindings) clone() bindings { 106 | out := make(bindings, len(b)) 107 | for k, v := range b { 108 | out[k] = v 109 | } 110 | return out 111 | } 112 | 113 | func (b bindings) merge(other bindings) bindings { 114 | for k, v := range other { 115 | b[k] = v 116 | } 117 | return b 118 | } 119 | 120 | func getMethod(value reflect.Value, name string) reflect.Value { 121 | method := value.MethodByName(name) 122 | if !method.IsValid() { 123 | if value.CanAddr() { 124 | method = value.Addr().MethodByName(name) 125 | } 126 | } 127 | return method 128 | } 129 | 130 | // getMethods gets all methods with the given name from the given value 131 | // and any embedded fields. 132 | // 133 | // Returns a slice of bound methods that can be called directly. 134 | func getMethods(value reflect.Value, name string) (methods []reflect.Value) { 135 | if value.Kind() == reflect.Ptr { 136 | value = value.Elem() 137 | } 138 | if !value.IsValid() { 139 | return 140 | } 141 | 142 | if method := getMethod(value, name); method.IsValid() { 143 | methods = append(methods, method) 144 | } 145 | 146 | if value.Kind() != reflect.Struct { 147 | return 148 | } 149 | // If the current value is a struct, also consider embedded fields. 150 | // Two kinds of embedded fields are considered if they're exported: 151 | // 152 | // - standard Go embedded fields 153 | // - fields tagged with `embed:""` 154 | t := value.Type() 155 | for i := 0; i < value.NumField(); i++ { 156 | fieldValue := value.Field(i) 157 | field := t.Field(i) 158 | 159 | if !field.IsExported() { 160 | continue 161 | } 162 | 163 | // Consider a field embedded if it's actually embedded 164 | // or if it's tagged with `embed:""`. 165 | _, isEmbedded := field.Tag.Lookup("embed") 166 | isEmbedded = isEmbedded || field.Anonymous 167 | if isEmbedded { 168 | methods = append(methods, getMethods(fieldValue, name)...) 169 | } 170 | } 171 | return 172 | } 173 | 174 | func callFunction(f reflect.Value, bindings bindings) error { 175 | if f.Kind() != reflect.Func { 176 | return fmt.Errorf("expected function, got %s", f.Type()) 177 | } 178 | t := f.Type() 179 | if t.NumOut() != 1 || !t.Out(0).Implements(callbackReturnSignature) { 180 | return fmt.Errorf("return value of %s must implement \"error\"", t) 181 | } 182 | out, err := callAnyFunction(f, bindings) 183 | if err != nil { 184 | return err 185 | } 186 | ferr := out[0] 187 | if ferrv := reflect.ValueOf(ferr); !ferrv.IsValid() || ((ferrv.Kind() == reflect.Interface || ferrv.Kind() == reflect.Pointer) && ferrv.IsNil()) { 188 | return nil 189 | } 190 | return ferr.(error) //nolint:forcetypeassert 191 | } 192 | 193 | func callAnyFunction(f reflect.Value, bindings bindings) (out []any, err error) { 194 | if f.Kind() != reflect.Func { 195 | return nil, fmt.Errorf("expected function, got %s", f.Type()) 196 | } 197 | in := []reflect.Value{} 198 | t := f.Type() 199 | for i := 0; i < t.NumIn(); i++ { 200 | pt := t.In(i) 201 | binding, ok := bindings[pt] 202 | if !ok { 203 | return nil, fmt.Errorf("couldn't find binding of type %s for parameter %d of %s(), use kong.Bind(%s)", pt, i, t, pt) 204 | } 205 | 206 | // Don't need to call the function if the value is already resolved. 207 | if val, ok := binding.Get(); ok { 208 | in = append(in, val) 209 | continue 210 | } 211 | 212 | // Recursively resolve binding functions. 213 | argv, err := callAnyFunction(binding.fn, bindings) 214 | if err != nil { 215 | return nil, fmt.Errorf("%s: %w", pt, err) 216 | } 217 | if ferrv := reflect.ValueOf(argv[len(argv)-1]); ferrv.IsValid() && ferrv.Type().Implements(callbackReturnSignature) && !ferrv.IsNil() { 218 | return nil, ferrv.Interface().(error) //nolint:forcetypeassert 219 | } 220 | 221 | val := reflect.ValueOf(argv[0]) 222 | binding.Set(val) 223 | in = append(in, val) 224 | } 225 | outv := f.Call(in) 226 | out = make([]any, len(outv)) 227 | for i, v := range outv { 228 | out[i] = v.Interface() 229 | } 230 | return out, nil 231 | } 232 | -------------------------------------------------------------------------------- /camelcase.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | // NOTE: This code is from https://github.com/fatih/camelcase. MIT license. 4 | 5 | import ( 6 | "unicode" 7 | "unicode/utf8" 8 | ) 9 | 10 | // Split splits the camelcase word and returns a list of words. It also 11 | // supports digits. Both lower camel case and upper camel case are supported. 12 | // For more info please check: http://en.wikipedia.org/wiki/CamelCase 13 | // 14 | // Examples 15 | // 16 | // "" => [""] 17 | // "lowercase" => ["lowercase"] 18 | // "Class" => ["Class"] 19 | // "MyClass" => ["My", "Class"] 20 | // "MyC" => ["My", "C"] 21 | // "HTML" => ["HTML"] 22 | // "PDFLoader" => ["PDF", "Loader"] 23 | // "AString" => ["A", "String"] 24 | // "SimpleXMLParser" => ["Simple", "XML", "Parser"] 25 | // "vimRPCPlugin" => ["vim", "RPC", "Plugin"] 26 | // "GL11Version" => ["GL", "11", "Version"] 27 | // "99Bottles" => ["99", "Bottles"] 28 | // "May5" => ["May", "5"] 29 | // "BFG9000" => ["BFG", "9000"] 30 | // "BöseÜberraschung" => ["Böse", "Überraschung"] 31 | // "Two spaces" => ["Two", " ", "spaces"] 32 | // "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"] 33 | // 34 | // Splitting rules 35 | // 36 | // 1. If string is not valid UTF-8, return it without splitting as 37 | // single item array. 38 | // 2. Assign all unicode characters into one of 4 sets: lower case 39 | // letters, upper case letters, numbers, and all other characters. 40 | // 3. Iterate through characters of string, introducing splits 41 | // between adjacent characters that belong to different sets. 42 | // 4. Iterate through array of split strings, and if a given string 43 | // is upper case: 44 | // if subsequent string is lower case: 45 | // move last character of upper case string to beginning of 46 | // lower case string 47 | func camelCase(src string) (entries []string) { 48 | // don't split invalid utf8 49 | if !utf8.ValidString(src) { 50 | return []string{src} 51 | } 52 | entries = []string{} 53 | var runes [][]rune 54 | lastClass := 0 55 | // split into fields based on class of unicode character 56 | for _, r := range src { 57 | var class int 58 | switch { 59 | case unicode.IsLower(r): 60 | class = 1 61 | case unicode.IsUpper(r): 62 | class = 2 63 | case unicode.IsDigit(r): 64 | class = 3 65 | default: 66 | class = 4 67 | } 68 | if class == lastClass { 69 | runes[len(runes)-1] = append(runes[len(runes)-1], r) 70 | } else { 71 | runes = append(runes, []rune{r}) 72 | } 73 | lastClass = class 74 | } 75 | // handle upper case -> lower case sequences, e.g. 76 | // "PDFL", "oader" -> "PDF", "Loader" 77 | for i := 0; i < len(runes)-1; i++ { 78 | if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) { 79 | runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...) 80 | runes[i] = runes[i][:len(runes[i])-1] 81 | } 82 | } 83 | // construct []string from results 84 | for _, s := range runes { 85 | if len(s) > 0 { 86 | entries = append(entries, string(s)) 87 | } 88 | } 89 | return entries 90 | } 91 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package kong_test 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | "github.com/alecthomas/kong" 10 | ) 11 | 12 | func TestMultipleConfigLoading(t *testing.T) { 13 | var cli struct { 14 | Flag string `json:"flag,omitempty"` 15 | } 16 | 17 | cli.Flag = "first" 18 | first := makeConfig(t, &cli) 19 | 20 | cli.Flag = "" 21 | second := makeConfig(t, &cli) 22 | 23 | p := mustNew(t, &cli, kong.Configuration(kong.JSON, first, second)) 24 | _, err := p.Parse(nil) 25 | assert.NoError(t, err) 26 | assert.Equal(t, "first", cli.Flag) 27 | } 28 | 29 | func TestConfigValidation(t *testing.T) { 30 | var cli struct { 31 | Flag string `json:"flag,omitempty" enum:"valid" required:""` 32 | } 33 | 34 | cli.Flag = "invalid" 35 | conf := makeConfig(t, &cli) 36 | 37 | p := mustNew(t, &cli, kong.Configuration(kong.JSON, conf)) 38 | _, err := p.Parse(nil) 39 | assert.Error(t, err) 40 | } 41 | 42 | func makeConfig(t *testing.T, config any) (path string) { 43 | t.Helper() 44 | w, err := os.CreateTemp(t.TempDir(), "") 45 | assert.NoError(t, err) 46 | defer w.Close() 47 | err = json.NewEncoder(w).Encode(config) 48 | assert.NoError(t, err) 49 | return w.Name() 50 | } 51 | -------------------------------------------------------------------------------- /defaults.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | // ApplyDefaults if they are not already set. 4 | func ApplyDefaults(target any, options ...Option) error { 5 | app, err := New(target, options...) 6 | if err != nil { 7 | return err 8 | } 9 | ctx, err := Trace(app, nil) 10 | if err != nil { 11 | return err 12 | } 13 | err = ctx.Resolve() 14 | if err != nil { 15 | return err 16 | } 17 | if err = ctx.ApplyDefaults(); err != nil { 18 | return err 19 | } 20 | return ctx.Validate() 21 | } 22 | -------------------------------------------------------------------------------- /defaults_test.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | ) 9 | 10 | func TestApplyDefaults(t *testing.T) { 11 | type CLI struct { 12 | Str string `default:"str"` 13 | Duration time.Duration `default:"30s"` 14 | } 15 | tests := []struct { 16 | name string 17 | target CLI 18 | expected CLI 19 | }{ 20 | {name: "DefaultsWhenNotSet", 21 | expected: CLI{Str: "str", Duration: time.Second * 30}}, 22 | {name: "PartiallySetDefaults", 23 | target: CLI{Duration: time.Second}, 24 | expected: CLI{Str: "str", Duration: time.Second}}, 25 | } 26 | for _, tt := range tests { 27 | tt := tt 28 | t.Run(tt.name, func(t *testing.T) { 29 | err := ApplyDefaults(&tt.target) 30 | assert.NoError(t, err) 31 | assert.Equal(t, tt.expected, tt.target) 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package kong aims to support arbitrarily complex command-line structures with as little developer effort as possible. 2 | // 3 | // Here's an example: 4 | // 5 | // shell rm [-f] [-r] ... 6 | // shell ls [ ...] 7 | // 8 | // This can be represented by the following command-line structure: 9 | // 10 | // package main 11 | // 12 | // import "github.com/alecthomas/kong" 13 | // 14 | // var CLI struct { 15 | // Rm struct { 16 | // Force bool `short:"f" help:"Force removal."` 17 | // Recursive bool `short:"r" help:"Recursively remove files."` 18 | // 19 | // Paths []string `arg help:"Paths to remove." type:"path"` 20 | // } `cmd help:"Remove files."` 21 | // 22 | // Ls struct { 23 | // Paths []string `arg optional help:"Paths to list." type:"path"` 24 | // } `cmd help:"List paths."` 25 | // } 26 | // 27 | // func main() { 28 | // kong.Parse(&CLI) 29 | // } 30 | // 31 | // See https://github.com/alecthomas/kong for details. 32 | package kong 33 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | // ParseError is the error type returned by Kong.Parse(). 4 | // 5 | // It contains the parse Context that triggered the error. 6 | type ParseError struct { 7 | error 8 | Context *Context 9 | exitCode int 10 | } 11 | 12 | // Unwrap returns the original cause of the error. 13 | func (p *ParseError) Unwrap() error { return p.error } 14 | 15 | // ExitCode returns the status that Kong should exit with if it fails with a ParseError. 16 | func (p *ParseError) ExitCode() int { 17 | if p.exitCode == 0 { 18 | return exitNotOk 19 | } 20 | return p.exitCode 21 | } 22 | -------------------------------------------------------------------------------- /exit.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import "errors" 4 | 5 | const ( 6 | exitOk = 0 7 | exitNotOk = 1 8 | 9 | // Semantic exit codes from https://github.com/square/exit?tab=readme-ov-file#about 10 | exitUsageError = 80 11 | ) 12 | 13 | // ExitCoder is an interface that may be implemented by an error value to 14 | // provide an integer exit code. The method ExitCode should return an integer 15 | // that is intended to be used as the exit code for the application. 16 | type ExitCoder interface { 17 | ExitCode() int 18 | } 19 | 20 | // exitCodeFromError returns the exit code for the given error. 21 | // If err implements the exitCoder interface, the ExitCode method is called. 22 | // Otherwise, exitCodeFromError returns 0 if err is nil, and 1 if it is not. 23 | func exitCodeFromError(err error) int { 24 | var e ExitCoder 25 | if errors.As(err, &e) { 26 | return e.ExitCode() 27 | } else if err == nil { 28 | return exitOk 29 | } 30 | 31 | return exitNotOk 32 | } 33 | -------------------------------------------------------------------------------- /global.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // Parse constructs a new parser and parses the default command-line. 8 | func Parse(cli any, options ...Option) *Context { 9 | parser, err := New(cli, options...) 10 | if err != nil { 11 | panic(err) 12 | } 13 | ctx, err := parser.Parse(os.Args[1:]) 14 | parser.FatalIfErrorf(err) 15 | return ctx 16 | } 17 | -------------------------------------------------------------------------------- /global_test.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | ) 9 | 10 | func TestParseHandlingBadBuild(t *testing.T) { 11 | var cli struct { 12 | Enabled bool `kong:"fail='"` 13 | } 14 | 15 | args := os.Args 16 | defer func() { 17 | os.Args = args 18 | }() 19 | 20 | os.Args = []string{os.Args[0], "hi"} 21 | 22 | defer func() { 23 | if r := recover(); r != nil { 24 | assert.Equal(t, "fail=' is not quoted properly", r.(error).Error()) //nolint 25 | } 26 | }() 27 | 28 | Parse(&cli, Exit(func(_ int) { panic("exiting") })) 29 | 30 | t.Fatal("we were expecting a panic") 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/alecthomas/kong 2 | 3 | require ( 4 | github.com/alecthomas/assert/v2 v2.11.0 5 | github.com/alecthomas/repr v0.4.0 6 | ) 7 | 8 | require github.com/hexops/gotextdiff v1.0.3 // indirect 9 | 10 | go 1.20 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 2 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 3 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 4 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 5 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 6 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 7 | -------------------------------------------------------------------------------- /guesswidth.go: -------------------------------------------------------------------------------- 1 | //go:build appengine || (!linux && !freebsd && !darwin && !dragonfly && !netbsd && !openbsd) 2 | // +build appengine !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd 3 | 4 | package kong 5 | 6 | import "io" 7 | 8 | func guessWidth(w io.Writer) int { 9 | return 80 10 | } 11 | -------------------------------------------------------------------------------- /guesswidth_unix.go: -------------------------------------------------------------------------------- 1 | //go:build (!appengine && linux) || freebsd || darwin || dragonfly || netbsd || openbsd 2 | // +build !appengine,linux freebsd darwin dragonfly netbsd openbsd 3 | 4 | package kong 5 | 6 | import ( 7 | "io" 8 | "os" 9 | "strconv" 10 | "syscall" 11 | "unsafe" 12 | ) 13 | 14 | func guessWidth(w io.Writer) int { 15 | // check if COLUMNS env is set to comply with 16 | // http://pubs.opengroup.org/onlinepubs/009604499/basedefs/xbd_chap08.html 17 | colsStr := os.Getenv("COLUMNS") 18 | if colsStr != "" { 19 | if cols, err := strconv.Atoi(colsStr); err == nil { 20 | return cols 21 | } 22 | } 23 | 24 | if t, ok := w.(*os.File); ok { 25 | fd := t.Fd() 26 | var dimensions [4]uint16 27 | 28 | if _, _, err := syscall.Syscall6( 29 | syscall.SYS_IOCTL, 30 | uintptr(fd), //nolint: unconvert 31 | uintptr(syscall.TIOCGWINSZ), 32 | uintptr(unsafe.Pointer(&dimensions)), //nolint: gas 33 | 0, 0, 0, 34 | ); err == 0 { 35 | if dimensions[1] == 0 { 36 | return 80 37 | } 38 | return int(dimensions[1]) 39 | } 40 | } 41 | return 80 42 | } 43 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/doc" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | defaultIndent = 2 13 | defaultColumnPadding = 4 14 | ) 15 | 16 | // Help flag. 17 | type helpFlag bool 18 | 19 | func (h helpFlag) IgnoreDefault() {} 20 | 21 | func (h helpFlag) BeforeReset(ctx *Context) error { 22 | options := ctx.Kong.helpOptions 23 | options.Summary = false 24 | err := ctx.Kong.help(options, ctx) 25 | if err != nil { 26 | return err 27 | } 28 | ctx.Kong.Exit(0) 29 | return nil 30 | } 31 | 32 | // HelpOptions for HelpPrinters. 33 | type HelpOptions struct { 34 | // Don't print top-level usage summary. 35 | NoAppSummary bool 36 | 37 | // Write a one-line summary of the context. 38 | Summary bool 39 | 40 | // Write help in a more compact, but still fully-specified, form. 41 | Compact bool 42 | 43 | // Tree writes command chains in a tree structure instead of listing them separately. 44 | Tree bool 45 | 46 | // Place the flags after the commands listing. 47 | FlagsLast bool 48 | 49 | // Indenter modulates the given prefix for the next layer in the tree view. 50 | // The following exported templates can be used: kong.SpaceIndenter, kong.LineIndenter, kong.TreeIndenter 51 | // The kong.SpaceIndenter will be used by default. 52 | Indenter HelpIndenter 53 | 54 | // Don't show the help associated with subcommands 55 | NoExpandSubcommands bool 56 | 57 | // Clamp the help wrap width to a value smaller than the terminal width. 58 | // If this is set to a non-positive number, the terminal width is used; otherwise, 59 | // the min of this value or the terminal width is used. 60 | WrapUpperBound int 61 | } 62 | 63 | // Apply options to Kong as a configuration option. 64 | func (h HelpOptions) Apply(k *Kong) error { 65 | k.helpOptions = h 66 | return nil 67 | } 68 | 69 | // HelpProvider can be implemented by commands/args to provide detailed help. 70 | type HelpProvider interface { 71 | // This string is formatted by go/doc and thus has the same formatting rules. 72 | Help() string 73 | } 74 | 75 | // PlaceHolderProvider can be implemented by mappers to provide custom placeholder text. 76 | type PlaceHolderProvider interface { 77 | PlaceHolder(flag *Flag) string 78 | } 79 | 80 | // HelpIndenter is used to indent new layers in the help tree. 81 | type HelpIndenter func(prefix string) string 82 | 83 | // HelpPrinter is used to print context-sensitive help. 84 | type HelpPrinter func(options HelpOptions, ctx *Context) error 85 | 86 | // HelpValueFormatter is used to format the help text of flags and positional arguments. 87 | type HelpValueFormatter func(value *Value) string 88 | 89 | // DefaultHelpValueFormatter is the default HelpValueFormatter. 90 | func DefaultHelpValueFormatter(value *Value) string { 91 | if len(value.Tag.Envs) == 0 || HasInterpolatedVar(value.OrigHelp, "env") { 92 | return value.Help 93 | } 94 | suffix := "(" + formatEnvs(value.Tag.Envs) + ")" 95 | switch { 96 | case strings.HasSuffix(value.Help, "."): 97 | return value.Help[:len(value.Help)-1] + " " + suffix + "." 98 | case value.Help == "": 99 | return suffix 100 | default: 101 | return value.Help + " " + suffix 102 | } 103 | } 104 | 105 | // DefaultShortHelpPrinter is the default HelpPrinter for short help on error. 106 | func DefaultShortHelpPrinter(options HelpOptions, ctx *Context) error { 107 | w := newHelpWriter(ctx, options) 108 | cmd := ctx.Selected() 109 | app := ctx.Model 110 | if cmd == nil { 111 | w.Printf("Usage: %s%s", app.Name, app.Summary()) 112 | w.Printf(`Run "%s --help" for more information.`, app.Name) 113 | } else { 114 | w.Printf("Usage: %s %s", app.Name, cmd.Summary()) 115 | w.Printf(`Run "%s --help" for more information.`, cmd.FullPath()) 116 | } 117 | return w.Write(ctx.Stdout) 118 | } 119 | 120 | // DefaultHelpPrinter is the default HelpPrinter. 121 | func DefaultHelpPrinter(options HelpOptions, ctx *Context) error { 122 | if ctx.Empty() { 123 | options.Summary = false 124 | } 125 | w := newHelpWriter(ctx, options) 126 | selected := ctx.Selected() 127 | if selected == nil { 128 | printApp(w, ctx.Model) 129 | } else { 130 | printCommand(w, ctx.Model, selected) 131 | } 132 | return w.Write(ctx.Stdout) 133 | } 134 | 135 | func printApp(w *helpWriter, app *Application) { 136 | if !w.NoAppSummary { 137 | w.Printf("Usage: %s%s", app.Name, app.Summary()) 138 | } 139 | printNodeDetail(w, app.Node, true) 140 | cmds := app.Leaves(true) 141 | if len(cmds) > 0 && app.HelpFlag != nil { 142 | w.Print("") 143 | if w.Summary { 144 | w.Printf(`Run "%s --help" for more information.`, app.Name) 145 | } else { 146 | w.Printf(`Run "%s --help" for more information on a command.`, app.Name) 147 | } 148 | } 149 | } 150 | 151 | func printCommand(w *helpWriter, app *Application, cmd *Command) { 152 | if !w.NoAppSummary { 153 | w.Printf("Usage: %s %s", app.Name, cmd.Summary()) 154 | } 155 | printNodeDetail(w, cmd, true) 156 | if w.Summary && app.HelpFlag != nil { 157 | w.Print("") 158 | w.Printf(`Run "%s --help" for more information.`, cmd.FullPath()) 159 | } 160 | } 161 | 162 | func printNodeDetail(w *helpWriter, node *Node, hide bool) { 163 | if node.Help != "" { 164 | w.Print("") 165 | w.Wrap(node.Help) 166 | } 167 | if w.Summary { 168 | return 169 | } 170 | if node.Detail != "" { 171 | w.Print("") 172 | w.Wrap(node.Detail) 173 | } 174 | if len(node.Positional) > 0 { 175 | w.Print("") 176 | w.Print("Arguments:") 177 | writePositionals(w.Indent(), node.Positional) 178 | } 179 | printFlags := func() { 180 | if flags := node.AllFlags(true); len(flags) > 0 { 181 | groupedFlags := collectFlagGroups(flags) 182 | for _, group := range groupedFlags { 183 | w.Print("") 184 | if group.Metadata.Title != "" { 185 | w.Wrap(group.Metadata.Title) 186 | } 187 | if group.Metadata.Description != "" { 188 | w.Indent().Wrap(group.Metadata.Description) 189 | w.Print("") 190 | } 191 | writeFlags(w.Indent(), group.Flags) 192 | } 193 | } 194 | } 195 | if !w.FlagsLast { 196 | printFlags() 197 | } 198 | var cmds []*Node 199 | if w.NoExpandSubcommands { 200 | cmds = node.Children 201 | } else { 202 | cmds = node.Leaves(hide) 203 | } 204 | if len(cmds) > 0 { 205 | iw := w.Indent() 206 | if w.Tree { 207 | w.Print("") 208 | w.Print("Commands:") 209 | writeCommandTree(iw, node) 210 | } else { 211 | groupedCmds := collectCommandGroups(cmds) 212 | for _, group := range groupedCmds { 213 | w.Print("") 214 | if group.Metadata.Title != "" { 215 | w.Wrap(group.Metadata.Title) 216 | } 217 | if group.Metadata.Description != "" { 218 | w.Indent().Wrap(group.Metadata.Description) 219 | w.Print("") 220 | } 221 | 222 | if w.Compact { 223 | writeCompactCommandList(group.Commands, iw) 224 | } else { 225 | writeCommandList(group.Commands, iw) 226 | } 227 | } 228 | } 229 | } 230 | if w.FlagsLast { 231 | printFlags() 232 | } 233 | } 234 | 235 | func writeCommandList(cmds []*Node, iw *helpWriter) { 236 | for i, cmd := range cmds { 237 | if cmd.Hidden { 238 | continue 239 | } 240 | printCommandSummary(iw, cmd) 241 | if i != len(cmds)-1 { 242 | iw.Print("") 243 | } 244 | } 245 | } 246 | 247 | func writeCompactCommandList(cmds []*Node, iw *helpWriter) { 248 | rows := [][2]string{} 249 | for _, cmd := range cmds { 250 | if cmd.Hidden { 251 | continue 252 | } 253 | rows = append(rows, [2]string{cmd.Path(), cmd.Help}) 254 | } 255 | writeTwoColumns(iw, rows) 256 | } 257 | 258 | func writeCommandTree(w *helpWriter, node *Node) { 259 | rows := make([][2]string, 0, len(node.Children)*2) 260 | for i, cmd := range node.Children { 261 | if cmd.Hidden { 262 | continue 263 | } 264 | rows = append(rows, w.CommandTree(cmd, "")...) 265 | if i != len(node.Children)-1 { 266 | rows = append(rows, [2]string{"", ""}) 267 | } 268 | } 269 | writeTwoColumns(w, rows) 270 | } 271 | 272 | type helpFlagGroup struct { 273 | Metadata *Group 274 | Flags [][]*Flag 275 | } 276 | 277 | func collectFlagGroups(flags [][]*Flag) []helpFlagGroup { 278 | // Group keys in order of appearance. 279 | groups := []*Group{} 280 | // Flags grouped by their group key. 281 | flagsByGroup := map[string][][]*Flag{} 282 | 283 | for _, levelFlags := range flags { 284 | levelFlagsByGroup := map[string][]*Flag{} 285 | 286 | for _, flag := range levelFlags { 287 | key := "" 288 | if flag.Group != nil { 289 | key = flag.Group.Key 290 | groupAlreadySeen := false 291 | for _, group := range groups { 292 | if key == group.Key { 293 | groupAlreadySeen = true 294 | break 295 | } 296 | } 297 | if !groupAlreadySeen { 298 | groups = append(groups, flag.Group) 299 | } 300 | } 301 | 302 | levelFlagsByGroup[key] = append(levelFlagsByGroup[key], flag) 303 | } 304 | 305 | for key, flags := range levelFlagsByGroup { 306 | flagsByGroup[key] = append(flagsByGroup[key], flags) 307 | } 308 | } 309 | 310 | out := []helpFlagGroup{} 311 | // Ungrouped flags are always displayed first. 312 | if ungroupedFlags, ok := flagsByGroup[""]; ok { 313 | out = append(out, helpFlagGroup{ 314 | Metadata: &Group{Title: "Flags:"}, 315 | Flags: ungroupedFlags, 316 | }) 317 | } 318 | for _, group := range groups { 319 | out = append(out, helpFlagGroup{Metadata: group, Flags: flagsByGroup[group.Key]}) 320 | } 321 | return out 322 | } 323 | 324 | type helpCommandGroup struct { 325 | Metadata *Group 326 | Commands []*Node 327 | } 328 | 329 | func collectCommandGroups(nodes []*Node) []helpCommandGroup { 330 | // Groups in order of appearance. 331 | groups := []*Group{} 332 | // Nodes grouped by their group key. 333 | nodesByGroup := map[string][]*Node{} 334 | 335 | for _, node := range nodes { 336 | key := "" 337 | if group := node.ClosestGroup(); group != nil { 338 | key = group.Key 339 | if _, ok := nodesByGroup[key]; !ok { 340 | groups = append(groups, group) 341 | } 342 | } 343 | nodesByGroup[key] = append(nodesByGroup[key], node) 344 | } 345 | 346 | out := []helpCommandGroup{} 347 | // Ungrouped nodes are always displayed first. 348 | if ungroupedNodes, ok := nodesByGroup[""]; ok { 349 | out = append(out, helpCommandGroup{ 350 | Metadata: &Group{Title: "Commands:"}, 351 | Commands: ungroupedNodes, 352 | }) 353 | } 354 | for _, group := range groups { 355 | out = append(out, helpCommandGroup{Metadata: group, Commands: nodesByGroup[group.Key]}) 356 | } 357 | return out 358 | } 359 | 360 | func printCommandSummary(w *helpWriter, cmd *Command) { 361 | w.Print(cmd.Summary()) 362 | if cmd.Help != "" { 363 | w.Indent().Wrap(cmd.Help) 364 | } 365 | } 366 | 367 | type helpWriter struct { 368 | indent string 369 | width int 370 | lines *[]string 371 | helpFormatter HelpValueFormatter 372 | HelpOptions 373 | } 374 | 375 | func newHelpWriter(ctx *Context, options HelpOptions) *helpWriter { 376 | lines := []string{} 377 | wrapWidth := guessWidth(ctx.Stdout) 378 | if options.WrapUpperBound > 0 && wrapWidth > options.WrapUpperBound { 379 | wrapWidth = options.WrapUpperBound 380 | } 381 | w := &helpWriter{ 382 | indent: "", 383 | width: wrapWidth, 384 | lines: &lines, 385 | helpFormatter: ctx.Kong.helpFormatter, 386 | HelpOptions: options, 387 | } 388 | return w 389 | } 390 | 391 | func (h *helpWriter) Printf(format string, args ...any) { 392 | h.Print(fmt.Sprintf(format, args...)) 393 | } 394 | 395 | func (h *helpWriter) Print(text string) { 396 | *h.lines = append(*h.lines, strings.TrimRight(h.indent+text, " ")) 397 | } 398 | 399 | // Indent returns a new helpWriter indented by two characters. 400 | func (h *helpWriter) Indent() *helpWriter { 401 | return &helpWriter{indent: h.indent + " ", lines: h.lines, width: h.width - 2, HelpOptions: h.HelpOptions, helpFormatter: h.helpFormatter} 402 | } 403 | 404 | func (h *helpWriter) String() string { 405 | return strings.Join(*h.lines, "\n") 406 | } 407 | 408 | func (h *helpWriter) Write(w io.Writer) error { 409 | for _, line := range *h.lines { 410 | _, err := io.WriteString(w, line+"\n") 411 | if err != nil { 412 | return err 413 | } 414 | } 415 | return nil 416 | } 417 | 418 | func (h *helpWriter) Wrap(text string) { 419 | w := bytes.NewBuffer(nil) 420 | doc.ToText(w, strings.TrimSpace(text), "", " ", h.width) //nolint:staticcheck // cross-package links not possible 421 | for _, line := range strings.Split(strings.TrimSpace(w.String()), "\n") { 422 | h.Print(line) 423 | } 424 | } 425 | 426 | func writePositionals(w *helpWriter, args []*Positional) { 427 | rows := [][2]string{} 428 | for _, arg := range args { 429 | rows = append(rows, [2]string{arg.Summary(), w.helpFormatter(arg)}) 430 | } 431 | writeTwoColumns(w, rows) 432 | } 433 | 434 | func writeFlags(w *helpWriter, groups [][]*Flag) { 435 | rows := [][2]string{} 436 | haveShort := false 437 | for _, group := range groups { 438 | for _, flag := range group { 439 | if flag.Short != 0 { 440 | haveShort = true 441 | break 442 | } 443 | } 444 | } 445 | for i, group := range groups { 446 | if i > 0 { 447 | rows = append(rows, [2]string{"", ""}) 448 | } 449 | for _, flag := range group { 450 | if !flag.Hidden { 451 | rows = append(rows, [2]string{formatFlag(haveShort, flag), w.helpFormatter(flag.Value)}) 452 | } 453 | } 454 | } 455 | writeTwoColumns(w, rows) 456 | } 457 | 458 | func writeTwoColumns(w *helpWriter, rows [][2]string) { 459 | maxLeft := 375 * w.width / 1000 460 | if maxLeft < 30 { 461 | maxLeft = 30 462 | } 463 | // Find size of first column. 464 | leftSize := 0 465 | for _, row := range rows { 466 | if c := len(row[0]); c > leftSize && c < maxLeft { 467 | leftSize = c 468 | } 469 | } 470 | 471 | offsetStr := strings.Repeat(" ", leftSize+defaultColumnPadding) 472 | 473 | for _, row := range rows { 474 | buf := bytes.NewBuffer(nil) 475 | doc.ToText(buf, row[1], "", strings.Repeat(" ", defaultIndent), w.width-leftSize-defaultColumnPadding) //nolint:staticcheck // cross-package links not possible 476 | lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") 477 | 478 | line := fmt.Sprintf("%-*s", leftSize, row[0]) 479 | if len(row[0]) < maxLeft { 480 | line += fmt.Sprintf("%*s%s", defaultColumnPadding, "", lines[0]) 481 | lines = lines[1:] 482 | } 483 | w.Print(line) 484 | for _, line := range lines { 485 | w.Printf("%s%s", offsetStr, line) 486 | } 487 | } 488 | } 489 | 490 | // haveShort will be true if there are short flags present at all in the help. Useful for column alignment. 491 | func formatFlag(haveShort bool, flag *Flag) string { 492 | flagString := "" 493 | name := flag.Name 494 | isBool := flag.IsBool() 495 | isCounter := flag.IsCounter() 496 | 497 | short := "" 498 | if flag.Short != 0 { 499 | short = "-" + string(flag.Short) + ", " 500 | } else if haveShort { 501 | short = " " 502 | } 503 | 504 | if isBool && flag.Tag.Negatable == negatableDefault { 505 | name = "[no-]" + name 506 | } else if isBool && flag.Tag.Negatable != "" { 507 | name += "/" + flag.Tag.Negatable 508 | } 509 | 510 | flagString += fmt.Sprintf("%s--%s", short, name) 511 | 512 | if !isBool && !isCounter { 513 | flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder()) 514 | } 515 | return flagString 516 | } 517 | 518 | // CommandTree creates a tree with the given node name as root and its children's arguments and sub commands as leaves. 519 | func (h *HelpOptions) CommandTree(node *Node, prefix string) (rows [][2]string) { 520 | var nodeName string 521 | switch node.Type { 522 | default: 523 | nodeName += prefix + node.Name 524 | if len(node.Aliases) != 0 { 525 | nodeName += fmt.Sprintf(" (%s)", strings.Join(node.Aliases, ",")) 526 | } 527 | case ArgumentNode: 528 | nodeName += prefix + "<" + node.Name + ">" 529 | } 530 | rows = append(rows, [2]string{nodeName, node.Help}) 531 | if h.Indenter == nil { 532 | prefix = SpaceIndenter(prefix) 533 | } else { 534 | prefix = h.Indenter(prefix) 535 | } 536 | for _, arg := range node.Positional { 537 | rows = append(rows, [2]string{prefix + arg.Summary(), arg.Help}) 538 | } 539 | for _, subCmd := range node.Children { 540 | if subCmd.Hidden { 541 | continue 542 | } 543 | rows = append(rows, h.CommandTree(subCmd, prefix)...) 544 | } 545 | return 546 | } 547 | 548 | // SpaceIndenter adds a space indent to the given prefix. 549 | func SpaceIndenter(prefix string) string { 550 | return prefix + strings.Repeat(" ", defaultIndent) 551 | } 552 | 553 | // LineIndenter adds line points to every new indent. 554 | func LineIndenter(prefix string) string { 555 | if prefix == "" { 556 | return "- " 557 | } 558 | return strings.Repeat(" ", defaultIndent) + prefix 559 | } 560 | 561 | // TreeIndenter adds line points to every new indent and vertical lines to every layer. 562 | func TreeIndenter(prefix string) string { 563 | if prefix == "" { 564 | return "|- " 565 | } 566 | return "|" + strings.Repeat(" ", defaultIndent) + prefix 567 | } 568 | 569 | func formatEnvs(envs []string) string { 570 | formatted := make([]string, len(envs)) 571 | for i := range envs { 572 | formatted[i] = "$" + envs[i] 573 | } 574 | 575 | return strings.Join(formatted, ", ") 576 | } 577 | -------------------------------------------------------------------------------- /helpwrap1.18_test.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.19 2 | // +build !go1.19 3 | 4 | package kong_test 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | 10 | "github.com/alecthomas/assert/v2" 11 | "github.com/alecthomas/kong" 12 | ) 13 | 14 | func TestCustomWrap(t *testing.T) { 15 | var cli struct { 16 | Flag string `help:"A string flag with very long help that wraps a lot and is verbose and is really verbose."` 17 | } 18 | 19 | w := bytes.NewBuffer(nil) 20 | app := mustNew(t, &cli, 21 | kong.Name("test-app"), 22 | kong.Description("A test app."), 23 | kong.HelpOptions{ 24 | WrapUpperBound: 50, 25 | }, 26 | kong.Writers(w, w), 27 | kong.Exit(func(int) {}), 28 | ) 29 | 30 | _, err := app.Parse([]string{"--help"}) 31 | assert.NoError(t, err) 32 | expected := `Usage: test-app 33 | 34 | A test app. 35 | 36 | Flags: 37 | -h, --help Show context-sensitive 38 | help. 39 | --flag=STRING A string flag with very 40 | long help that wraps a lot 41 | and is verbose and is 42 | really verbose. 43 | ` 44 | t.Log(w.String()) 45 | t.Log(expected) 46 | assert.Equal(t, expected, w.String()) 47 | } 48 | -------------------------------------------------------------------------------- /helpwrap1.19_test.go: -------------------------------------------------------------------------------- 1 | // Wrapping of text changed in Go1.19 per https://github.com/alecthomas/kong/issues/325 2 | // The test has been split pre-go1.19 and go1.19 and onwards. 3 | 4 | //go:build go1.19 5 | // +build go1.19 6 | 7 | package kong_test 8 | 9 | import ( 10 | "bytes" 11 | "testing" 12 | 13 | "github.com/alecthomas/assert/v2" 14 | "github.com/alecthomas/kong" 15 | ) 16 | 17 | func TestCustomWrap(t *testing.T) { 18 | var cli struct { 19 | Flag string `help:"A string flag with very long help that wraps a lot and is verbose and is really verbose."` 20 | } 21 | 22 | w := bytes.NewBuffer(nil) 23 | app := mustNew(t, &cli, 24 | kong.Name("test-app"), 25 | kong.Description("A test app."), 26 | kong.HelpOptions{ 27 | WrapUpperBound: 50, 28 | }, 29 | kong.Writers(w, w), 30 | kong.Exit(func(int) {}), 31 | ) 32 | 33 | _, err := app.Parse([]string{"--help"}) 34 | assert.NoError(t, err) 35 | expected := `Usage: test-app [flags] 36 | 37 | A test app. 38 | 39 | Flags: 40 | -h, --help Show context-sensitive 41 | help. 42 | --flag=STRING A string flag with very 43 | long help that wraps a 44 | lot and is verbose and is 45 | really verbose. 46 | ` 47 | t.Log(w.String()) 48 | t.Log(expected) 49 | assert.Equal(t, expected, w.String()) 50 | } 51 | -------------------------------------------------------------------------------- /hooks.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | // BeforeReset is a documentation-only interface describing hooks that run before defaults values are applied. 4 | type BeforeReset interface { 5 | // This is not the correct signature - see README for details. 6 | BeforeReset(args ...any) error 7 | } 8 | 9 | // BeforeResolve is a documentation-only interface describing hooks that run before resolvers are applied. 10 | type BeforeResolve interface { 11 | // This is not the correct signature - see README for details. 12 | BeforeResolve(args ...any) error 13 | } 14 | 15 | // BeforeApply is a documentation-only interface describing hooks that run before values are set. 16 | type BeforeApply interface { 17 | // This is not the correct signature - see README for details. 18 | BeforeApply(args ...any) error 19 | } 20 | 21 | // AfterApply is a documentation-only interface describing hooks that run after values are set. 22 | type AfterApply interface { 23 | // This is not the correct signature - see README for details. 24 | AfterApply(args ...any) error 25 | } 26 | 27 | // AfterRun is a documentation-only interface describing hooks that run after Run() returns. 28 | type AfterRun interface { 29 | // This is not the correct signature - see README for details. 30 | // AfterRun is called after Run() returns. 31 | AfterRun(args ...any) error 32 | } 33 | -------------------------------------------------------------------------------- /interpolate.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | ) 7 | 8 | var interpolationRegex = regexp.MustCompile(`(\$\$)|((?:\${([[:alpha:]_][[:word:]]*))(?:=([^}]+))?})|(\$)|([^$]+)`) 9 | 10 | // HasInterpolatedVar returns true if the variable "v" is interpolated in "s". 11 | func HasInterpolatedVar(s string, v string) bool { 12 | matches := interpolationRegex.FindAllStringSubmatch(s, -1) 13 | for _, match := range matches { 14 | if name := match[3]; name == v { 15 | return true 16 | } 17 | } 18 | return false 19 | } 20 | 21 | // Interpolate variables from vars into s for substrings in the form ${var} or ${var=default}. 22 | func interpolate(s string, vars Vars, updatedVars map[string]string) (string, error) { 23 | out := "" 24 | matches := interpolationRegex.FindAllStringSubmatch(s, -1) 25 | if len(matches) == 0 { 26 | return s, nil 27 | } 28 | for key, val := range updatedVars { 29 | if vars[key] != val { 30 | vars = vars.CloneWith(updatedVars) 31 | break 32 | } 33 | } 34 | for _, match := range matches { 35 | if dollar := match[1]; dollar != "" { 36 | out += "$" 37 | } else if name := match[3]; name != "" { 38 | value, ok := vars[name] 39 | if !ok { 40 | // No default value. 41 | if match[4] == "" { 42 | return "", fmt.Errorf("undefined variable ${%s}", name) 43 | } 44 | value = match[4] 45 | } 46 | out += value 47 | } else { 48 | out += match[0] 49 | } 50 | } 51 | return out, nil 52 | } 53 | -------------------------------------------------------------------------------- /interpolate_test.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | ) 8 | 9 | func TestInterpolate(t *testing.T) { 10 | vars := map[string]string{ 11 | "age": "35", 12 | "city": "Melbourne", 13 | } 14 | updatedVars := map[string]string{ 15 | "height": "180", 16 | } 17 | actual, err := interpolate("${name=Bobby Brown} is ${age} years old, ${height} cm tall, lives in ${city=}, and likes $${AUD}", vars, updatedVars) 18 | assert.NoError(t, err) 19 | assert.Equal(t, `Bobby Brown is 35 years old, 180 cm tall, lives in Melbourne, and likes ${AUD}`, actual) 20 | } 21 | 22 | func TestHasInterpolatedVar(t *testing.T) { 23 | for _, tag := range []string{"name", "age", "height", "city"} { 24 | assert.True(t, HasInterpolatedVar("${name=Bobby Brown} is ${age} years old, ${height} cm tall, lives in ${city=}, and likes $${AUD}", tag), tag) 25 | } 26 | 27 | for _, tag := range []string{"name", "age", "height", "AUD"} { 28 | assert.False(t, HasInterpolatedVar("$name $$age {height} $${AUD}", tag), tag) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /kong.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | callbackReturnSignature = reflect.TypeOf((*error)(nil)).Elem() 16 | ) 17 | 18 | func failField(parent reflect.Value, field reflect.StructField, format string, args ...any) error { 19 | name := parent.Type().Name() 20 | if name == "" { 21 | name = "" 22 | } 23 | return fmt.Errorf("%s.%s: %s", name, field.Name, fmt.Sprintf(format, args...)) 24 | } 25 | 26 | // Must creates a new Parser or panics if there is an error. 27 | func Must(ast any, options ...Option) *Kong { 28 | k, err := New(ast, options...) 29 | if err != nil { 30 | panic(err) 31 | } 32 | return k 33 | } 34 | 35 | type usageOnError int 36 | 37 | const ( 38 | shortUsage usageOnError = iota + 1 39 | fullUsage 40 | ) 41 | 42 | // Kong is the main parser type. 43 | type Kong struct { 44 | // Grammar model. 45 | Model *Application 46 | 47 | // Termination function (defaults to os.Exit) 48 | Exit func(int) 49 | 50 | Stdout io.Writer 51 | Stderr io.Writer 52 | 53 | bindings bindings 54 | loader ConfigurationLoader 55 | resolvers []Resolver 56 | registry *Registry 57 | ignoreFields []*regexp.Regexp 58 | 59 | noDefaultHelp bool 60 | allowHyphenated bool 61 | usageOnError usageOnError 62 | help HelpPrinter 63 | shortHelp HelpPrinter 64 | helpFormatter HelpValueFormatter 65 | helpOptions HelpOptions 66 | helpFlag *Flag 67 | groups []Group 68 | vars Vars 69 | flagNamer func(string) string 70 | 71 | // Set temporarily by Options. These are applied after build(). 72 | postBuildOptions []Option 73 | embedded []embedded 74 | dynamicCommands []*dynamicCommand 75 | 76 | hooks map[string][]reflect.Value 77 | } 78 | 79 | // New creates a new Kong parser on grammar. 80 | // 81 | // See the README (https://github.com/alecthomas/kong) for usage instructions. 82 | func New(grammar any, options ...Option) (*Kong, error) { 83 | k := &Kong{ 84 | Exit: os.Exit, 85 | Stdout: os.Stdout, 86 | Stderr: os.Stderr, 87 | registry: NewRegistry().RegisterDefaults(), 88 | vars: Vars{}, 89 | bindings: bindings{}, 90 | hooks: make(map[string][]reflect.Value), 91 | helpFormatter: DefaultHelpValueFormatter, 92 | ignoreFields: make([]*regexp.Regexp, 0), 93 | flagNamer: func(s string) string { 94 | return strings.ToLower(dashedString(s)) 95 | }, 96 | } 97 | 98 | options = append(options, Bind(k)) 99 | 100 | for _, option := range options { 101 | if err := option.Apply(k); err != nil { 102 | return nil, err 103 | } 104 | } 105 | 106 | if k.help == nil { 107 | k.help = DefaultHelpPrinter 108 | } 109 | 110 | if k.shortHelp == nil { 111 | k.shortHelp = DefaultShortHelpPrinter 112 | } 113 | 114 | model, err := build(k, grammar) 115 | if err != nil { 116 | return k, err 117 | } 118 | model.Name = filepath.Base(os.Args[0]) 119 | k.Model = model 120 | k.Model.HelpFlag = k.helpFlag 121 | 122 | // Embed any embedded structs. 123 | for _, embed := range k.embedded { 124 | tag, err := parseTagString(strings.Join(embed.tags, " ")) 125 | if err != nil { 126 | return nil, err 127 | } 128 | tag.Embed = true 129 | v := reflect.Indirect(reflect.ValueOf(embed.strct)) 130 | node, err := buildNode(k, v, CommandNode, tag, map[string]bool{}) 131 | if err != nil { 132 | return nil, err 133 | } 134 | for _, child := range node.Children { 135 | child.Parent = k.Model.Node 136 | k.Model.Children = append(k.Model.Children, child) 137 | } 138 | k.Model.Flags = append(k.Model.Flags, node.Flags...) 139 | } 140 | 141 | // Synthesise command nodes. 142 | for _, dcmd := range k.dynamicCommands { 143 | tag, terr := parseTagString(strings.Join(dcmd.tags, " ")) 144 | if terr != nil { 145 | return nil, terr 146 | } 147 | tag.Name = dcmd.name 148 | tag.Help = dcmd.help 149 | tag.Group = dcmd.group 150 | tag.Cmd = true 151 | v := reflect.Indirect(reflect.ValueOf(dcmd.cmd)) 152 | err = buildChild(k, k.Model.Node, CommandNode, reflect.Value{}, reflect.StructField{ 153 | Name: dcmd.name, 154 | Type: v.Type(), 155 | }, v, tag, dcmd.name, map[string]bool{}) 156 | if err != nil { 157 | return nil, err 158 | } 159 | } 160 | 161 | for _, option := range k.postBuildOptions { 162 | if err = option.Apply(k); err != nil { 163 | return nil, err 164 | } 165 | } 166 | k.postBuildOptions = nil 167 | 168 | if err = k.interpolate(k.Model.Node); err != nil { 169 | return nil, err 170 | } 171 | 172 | k.bindings.add(k.vars) 173 | 174 | if err = checkOverlappingXorAnd(k); err != nil { 175 | return nil, err 176 | } 177 | 178 | return k, nil 179 | } 180 | 181 | func checkOverlappingXorAnd(k *Kong) error { 182 | xorGroups := map[string][]string{} 183 | andGroups := map[string][]string{} 184 | for _, flag := range k.Model.Node.Flags { 185 | for _, xor := range flag.Xor { 186 | xorGroups[xor] = append(xorGroups[xor], flag.Name) 187 | } 188 | for _, and := range flag.And { 189 | andGroups[and] = append(andGroups[and], flag.Name) 190 | } 191 | } 192 | for xor, xorSet := range xorGroups { 193 | for and, andSet := range andGroups { 194 | overlappingEntries := []string{} 195 | for _, xorTag := range xorSet { 196 | for _, andTag := range andSet { 197 | if xorTag == andTag { 198 | overlappingEntries = append(overlappingEntries, xorTag) 199 | } 200 | } 201 | } 202 | if len(overlappingEntries) > 1 { 203 | return fmt.Errorf("invalid xor and combination, %s and %s overlap with more than one: %s", xor, and, overlappingEntries) 204 | } 205 | } 206 | } 207 | return nil 208 | } 209 | 210 | type varStack []Vars 211 | 212 | func (v *varStack) head() Vars { return (*v)[len(*v)-1] } 213 | func (v *varStack) pop() { *v = (*v)[:len(*v)-1] } 214 | func (v *varStack) push(vars Vars) Vars { 215 | if len(*v) != 0 { 216 | vars = (*v)[len(*v)-1].CloneWith(vars) 217 | } 218 | *v = append(*v, vars) 219 | return vars 220 | } 221 | 222 | // Interpolate variables into model. 223 | func (k *Kong) interpolate(node *Node) (err error) { 224 | stack := varStack{} 225 | return Visit(node, func(node Visitable, next Next) error { 226 | switch node := node.(type) { 227 | case *Node: 228 | vars := stack.push(node.Vars()) 229 | node.Help, err = interpolate(node.Help, vars, nil) 230 | if err != nil { 231 | return fmt.Errorf("help for %s: %s", node.Path(), err) 232 | } 233 | err = next(nil) 234 | stack.pop() 235 | return err 236 | 237 | case *Value: 238 | return next(k.interpolateValue(node, stack.head())) 239 | } 240 | return next(nil) 241 | }) 242 | } 243 | 244 | func (k *Kong) interpolateValue(value *Value, vars Vars) (err error) { 245 | if len(value.Tag.Vars) > 0 { 246 | vars = vars.CloneWith(value.Tag.Vars) 247 | } 248 | if varsContributor, ok := value.Mapper.(VarsContributor); ok { 249 | vars = vars.CloneWith(varsContributor.Vars(value)) 250 | } 251 | 252 | if value.Enum, err = interpolate(value.Enum, vars, nil); err != nil { 253 | return fmt.Errorf("enum for %s: %s", value.Summary(), err) 254 | } 255 | 256 | if value.Default, err = interpolate(value.Default, vars, nil); err != nil { 257 | return fmt.Errorf("default value for %s: %s", value.Summary(), err) 258 | } 259 | if value.Enum, err = interpolate(value.Enum, vars, nil); err != nil { 260 | return fmt.Errorf("enum value for %s: %s", value.Summary(), err) 261 | } 262 | updatedVars := map[string]string{ 263 | "default": value.Default, 264 | "enum": value.Enum, 265 | } 266 | if value.Flag != nil { 267 | for i, env := range value.Flag.Envs { 268 | if value.Flag.Envs[i], err = interpolate(env, vars, updatedVars); err != nil { 269 | return fmt.Errorf("env value for %s: %s", value.Summary(), err) 270 | } 271 | } 272 | value.Tag.Envs = value.Flag.Envs 273 | updatedVars["env"] = "" 274 | if len(value.Flag.Envs) != 0 { 275 | updatedVars["env"] = value.Flag.Envs[0] 276 | } 277 | 278 | value.Flag.PlaceHolder, err = interpolate(value.Flag.PlaceHolder, vars, updatedVars) 279 | if err != nil { 280 | return fmt.Errorf("placeholder value for %s: %s", value.Summary(), err) 281 | } 282 | } 283 | value.Help, err = interpolate(value.Help, vars, updatedVars) 284 | if err != nil { 285 | return fmt.Errorf("help for %s: %s", value.Summary(), err) 286 | } 287 | return nil 288 | } 289 | 290 | // Provide additional builtin flags, if any. 291 | func (k *Kong) extraFlags() []*Flag { 292 | if k.noDefaultHelp { 293 | return nil 294 | } 295 | var helpTarget helpFlag 296 | value := reflect.ValueOf(&helpTarget).Elem() 297 | helpFlag := &Flag{ 298 | Short: 'h', 299 | Value: &Value{ 300 | Name: "help", 301 | Help: "Show context-sensitive help.", 302 | OrigHelp: "Show context-sensitive help.", 303 | Target: value, 304 | Tag: &Tag{}, 305 | Mapper: k.registry.ForValue(value), 306 | DefaultValue: reflect.ValueOf(false), 307 | }, 308 | } 309 | helpFlag.Flag = helpFlag 310 | k.helpFlag = helpFlag 311 | return []*Flag{helpFlag} 312 | } 313 | 314 | // Parse arguments into target. 315 | // 316 | // The return Context can be used to further inspect the parsed command-line, to format help, to find the 317 | // selected command, to run command Run() methods, and so on. See Context and README for more information. 318 | // 319 | // Will return a ParseError if a *semantically* invalid command-line is encountered (as opposed to a syntactically 320 | // invalid one, which will report a normal error). 321 | func (k *Kong) Parse(args []string) (ctx *Context, err error) { 322 | ctx, err = Trace(k, args) 323 | if err != nil { // Trace is not expected to return an err 324 | return nil, &ParseError{error: err, Context: ctx, exitCode: exitUsageError} 325 | } 326 | if ctx.Error != nil { 327 | return nil, &ParseError{error: ctx.Error, Context: ctx, exitCode: exitUsageError} 328 | } 329 | if err = k.applyHook(ctx, "BeforeReset"); err != nil { 330 | return nil, &ParseError{error: err, Context: ctx} 331 | } 332 | if err = ctx.Reset(); err != nil { 333 | return nil, &ParseError{error: err, Context: ctx} 334 | } 335 | if err = k.applyHook(ctx, "BeforeResolve"); err != nil { 336 | return nil, &ParseError{error: err, Context: ctx} 337 | } 338 | if err = ctx.Resolve(); err != nil { 339 | return nil, &ParseError{error: err, Context: ctx} 340 | } 341 | if err = k.applyHook(ctx, "BeforeApply"); err != nil { 342 | return nil, &ParseError{error: err, Context: ctx} 343 | } 344 | if _, err = ctx.Apply(); err != nil { // Apply is not expected to return an err 345 | return nil, &ParseError{error: err, Context: ctx} 346 | } 347 | if err = ctx.Validate(); err != nil { 348 | return nil, &ParseError{error: err, Context: ctx, exitCode: exitUsageError} 349 | } 350 | if err = k.applyHook(ctx, "AfterApply"); err != nil { 351 | return nil, &ParseError{error: err, Context: ctx} 352 | } 353 | return ctx, nil 354 | } 355 | 356 | func (k *Kong) applyHook(ctx *Context, name string) error { 357 | for _, trace := range ctx.Path { 358 | var value reflect.Value 359 | switch { 360 | case trace.App != nil: 361 | value = trace.App.Target 362 | case trace.Argument != nil: 363 | value = trace.Argument.Target 364 | case trace.Command != nil: 365 | value = trace.Command.Target 366 | case trace.Positional != nil: 367 | value = trace.Positional.Target 368 | case trace.Flag != nil: 369 | value = trace.Flag.Value.Target 370 | default: 371 | panic("unsupported Path") 372 | } 373 | for _, method := range k.getMethods(value, name) { 374 | binds := k.bindings.clone() 375 | binds.add(ctx, trace) 376 | binds.add(trace.Node().Vars().CloneWith(k.vars)) 377 | binds.merge(ctx.bindings) 378 | if err := callFunction(method, binds); err != nil { 379 | return err 380 | } 381 | } 382 | } 383 | // Path[0] will always be the app root. 384 | return k.applyHookToDefaultFlags(ctx, ctx.Path[0].Node(), name) 385 | } 386 | 387 | func (k *Kong) getMethods(value reflect.Value, name string) []reflect.Value { 388 | return append( 389 | // Identify callbacks by reflecting on value 390 | getMethods(value, name), 391 | 392 | // Identify callbacks that were registered with a kong.Option 393 | k.hooks[name]..., 394 | ) 395 | } 396 | 397 | // Call hook on any unset flags with default values. 398 | func (k *Kong) applyHookToDefaultFlags(ctx *Context, node *Node, name string) error { 399 | if node == nil { 400 | return nil 401 | } 402 | return Visit(node, func(n Visitable, next Next) error { 403 | node, ok := n.(*Node) 404 | if !ok { 405 | return next(nil) 406 | } 407 | binds := k.bindings.clone().add(ctx).add(node.Vars().CloneWith(k.vars)) 408 | for _, flag := range node.Flags { 409 | if !flag.HasDefault || ctx.values[flag.Value].IsValid() || !flag.Target.IsValid() { 410 | continue 411 | } 412 | for _, method := range getMethods(flag.Target, name) { 413 | path := &Path{Flag: flag} 414 | if err := callFunction(method, binds.clone().add(path)); err != nil { 415 | return next(err) 416 | } 417 | } 418 | } 419 | return next(nil) 420 | }) 421 | } 422 | 423 | func formatMultilineMessage(w io.Writer, leaders []string, format string, args ...any) { 424 | lines := strings.Split(strings.TrimRight(fmt.Sprintf(format, args...), "\n"), "\n") 425 | leader := "" 426 | for _, l := range leaders { 427 | if l == "" { 428 | continue 429 | } 430 | leader += l + ": " 431 | } 432 | fmt.Fprintf(w, "%s%s\n", leader, lines[0]) 433 | for _, line := range lines[1:] { 434 | fmt.Fprintf(w, "%*s%s\n", len(leader), " ", line) 435 | } 436 | } 437 | 438 | // Printf writes a message to Kong.Stdout with the application name prefixed. 439 | func (k *Kong) Printf(format string, args ...any) *Kong { 440 | formatMultilineMessage(k.Stdout, []string{k.Model.Name}, format, args...) 441 | return k 442 | } 443 | 444 | // Errorf writes a message to Kong.Stderr with the application name prefixed. 445 | func (k *Kong) Errorf(format string, args ...any) *Kong { 446 | formatMultilineMessage(k.Stderr, []string{k.Model.Name, "error"}, format, args...) 447 | return k 448 | } 449 | 450 | // Fatalf writes a message to Kong.Stderr with the application name prefixed then exits with status 1. 451 | func (k *Kong) Fatalf(format string, args ...any) { 452 | k.Errorf(format, args...) 453 | k.Exit(1) 454 | } 455 | 456 | // FatalIfErrorf terminates with an error message if err != nil. 457 | // If the error implements the ExitCoder interface, the ExitCode() method is called and 458 | // the application exits with that status. Otherwise, the application exits with status 1. 459 | func (k *Kong) FatalIfErrorf(err error, args ...any) { 460 | if err == nil { 461 | return 462 | } 463 | msg := err.Error() 464 | if len(args) > 0 { 465 | msg = fmt.Sprintf(args[0].(string), args[1:]...) + ": " + err.Error() //nolint 466 | } 467 | // Maybe display usage information. 468 | var parseErr *ParseError 469 | if errors.As(err, &parseErr) { 470 | switch k.usageOnError { 471 | case fullUsage: 472 | _ = k.help(k.helpOptions, parseErr.Context) 473 | fmt.Fprintln(k.Stdout) 474 | case shortUsage: 475 | _ = k.shortHelp(k.helpOptions, parseErr.Context) 476 | fmt.Fprintln(k.Stdout) 477 | } 478 | } 479 | k.Errorf("%s", msg) 480 | k.Exit(exitCodeFromError(err)) 481 | } 482 | 483 | // LoadConfig from path using the loader configured via Configuration(loader). 484 | // 485 | // "path" will have ~ and any variables expanded. 486 | func (k *Kong) LoadConfig(path string) (Resolver, error) { 487 | var err error 488 | path = ExpandPath(path) 489 | path, err = interpolate(path, k.vars, nil) 490 | if err != nil { 491 | return nil, err 492 | } 493 | r, err := os.Open(path) //nolint: gas 494 | if err != nil { 495 | return nil, err 496 | } 497 | defer r.Close() 498 | 499 | return k.loader(r) 500 | } 501 | -------------------------------------------------------------------------------- /kong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alecthomas/kong/9bc3bf9925397be48270da0e258bfb0a4f6ed96a/kong.png -------------------------------------------------------------------------------- /kong.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alecthomas/kong/9bc3bf9925397be48270da0e258bfb0a4f6ed96a/kong.sketch -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | output: 2 | - success 3 | - failure 4 | pre-push: 5 | parallel: true 6 | jobs: 7 | - name: test 8 | run: go test -v ./... 9 | 10 | - name: lint 11 | run: golangci-lint run 12 | -------------------------------------------------------------------------------- /levenshtein.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import "unicode/utf8" 4 | 5 | // https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Go 6 | // License: https://creativecommons.org/licenses/by-sa/3.0/ 7 | func levenshtein(a, b string) int { 8 | f := make([]int, utf8.RuneCountInString(b)+1) 9 | 10 | for j := range f { 11 | f[j] = j 12 | } 13 | 14 | for _, ca := range a { 15 | j := 1 16 | fj1 := f[0] // fj1 is the value of f[j - 1] in last iteration 17 | f[0]++ 18 | for _, cb := range b { 19 | mn := min(f[j]+1, f[j-1]+1) // delete & insert 20 | if cb != ca { 21 | mn = min(mn, fj1+1) // change 22 | } else { 23 | mn = min(mn, fj1) // matched 24 | } 25 | 26 | fj1, f[j] = f[j], mn // save f[j] to fj1(j is about to increase), update f[j] to mn 27 | j++ 28 | } 29 | } 30 | 31 | return f[len(f)-1] 32 | } 33 | 34 | func min(a, b int) int { //nolint:predeclared 35 | if a <= b { 36 | return a 37 | } 38 | return b 39 | } 40 | -------------------------------------------------------------------------------- /mapper_linux_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package kong_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/alecthomas/assert/v2" 10 | ) 11 | 12 | func TestPathMapper(t *testing.T) { 13 | var cli struct { 14 | Path string `arg:"" type:"path"` 15 | } 16 | p := mustNew(t, &cli) 17 | 18 | _, err := p.Parse([]string{"/an/absolute/path"}) 19 | assert.NoError(t, err) 20 | assert.Equal(t, "/an/absolute/path", cli.Path) 21 | 22 | _, err = p.Parse([]string{"-"}) 23 | assert.NoError(t, err) 24 | assert.Equal(t, "-", cli.Path) 25 | } 26 | -------------------------------------------------------------------------------- /mapper_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package kong_test 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | 10 | "github.com/alecthomas/assert/v2" 11 | ) 12 | 13 | func TestWindowsPathMapper(t *testing.T) { 14 | var cli struct { 15 | Path string `short:"p" type:"path"` 16 | Files []string `short:"f" type:"path"` 17 | } 18 | wd, err := os.Getwd() 19 | assert.NoError(t, err, "Getwd failed") 20 | p := mustNew(t, &cli) 21 | 22 | _, err = p.Parse([]string{`-p`, `c:\an\absolute\path`, `-f`, `c:\second\absolute\path\`, `-f`, `relative\path\file`}) 23 | assert.NoError(t, err) 24 | assert.Equal(t, `c:\an\absolute\path`, cli.Path) 25 | assert.Equal(t, []string{`c:\second\absolute\path\`, wd + `\relative\path\file`}, cli.Files) 26 | } 27 | 28 | func TestWindowsFileMapper(t *testing.T) { 29 | type CLI struct { 30 | File *os.File `arg:""` 31 | } 32 | var cli CLI 33 | p := mustNew(t, &cli) 34 | _, err := p.Parse([]string{"testdata\\file.txt"}) 35 | assert.NoError(t, err) 36 | assert.NotZero(t, cli.File, "File should not be nil") 37 | _ = cli.File.Close() 38 | _, err = p.Parse([]string{"testdata\\missing.txt"}) 39 | assert.Error(t, err) 40 | assert.Contains(t, err.Error(), "missing.txt:") 41 | assert.IsError(t, err, os.ErrNotExist) 42 | _, err = p.Parse([]string{"-"}) 43 | assert.NoError(t, err) 44 | assert.Equal(t, os.Stdin, cli.File) 45 | } 46 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // A Visitable component in the model. 13 | type Visitable interface { 14 | node() 15 | } 16 | 17 | // Application is the root of the Kong model. 18 | type Application struct { 19 | *Node 20 | // Help flag, if the NoDefaultHelp() option is not specified. 21 | HelpFlag *Flag 22 | } 23 | 24 | // Argument represents a branching positional argument. 25 | type Argument = Node 26 | 27 | // Command represents a command in the CLI. 28 | type Command = Node 29 | 30 | // NodeType is an enum representing the type of a Node. 31 | type NodeType int 32 | 33 | // Node type enumerations. 34 | const ( 35 | ApplicationNode NodeType = iota 36 | CommandNode 37 | ArgumentNode 38 | ) 39 | 40 | // Node is a branch in the CLI. ie. a command or positional argument. 41 | type Node struct { 42 | Type NodeType 43 | Parent *Node 44 | Name string 45 | Help string // Short help displayed in summaries. 46 | Detail string // Detailed help displayed when describing command/arg alone. 47 | Group *Group 48 | Hidden bool 49 | Flags []*Flag 50 | Positional []*Positional 51 | Children []*Node 52 | DefaultCmd *Node 53 | Target reflect.Value // Pointer to the value in the grammar that this Node is associated with. 54 | Tag *Tag 55 | Aliases []string 56 | Passthrough bool // Set to true to stop flag parsing when encountered. 57 | Active bool // Denotes the node is part of an active branch in the CLI. 58 | 59 | Argument *Value // Populated when Type is ArgumentNode. 60 | } 61 | 62 | func (*Node) node() {} 63 | 64 | // Leaf returns true if this Node is a leaf node. 65 | func (n *Node) Leaf() bool { 66 | return len(n.Children) == 0 67 | } 68 | 69 | // Find a command/argument/flag by pointer to its field. 70 | // 71 | // Returns nil if not found. Panics if ptr is not a pointer. 72 | func (n *Node) Find(ptr any) *Node { 73 | key := reflect.ValueOf(ptr) 74 | if key.Kind() != reflect.Ptr { 75 | panic("expected a pointer") 76 | } 77 | return n.findNode(key) 78 | } 79 | 80 | func (n *Node) findNode(key reflect.Value) *Node { 81 | if n.Target == key { 82 | return n 83 | } 84 | for _, child := range n.Children { 85 | if found := child.findNode(key); found != nil { 86 | return found 87 | } 88 | } 89 | return nil 90 | } 91 | 92 | // AllFlags returns flags from all ancestor branches encountered. 93 | // 94 | // If "hide" is true hidden flags will be omitted. 95 | func (n *Node) AllFlags(hide bool) (out [][]*Flag) { 96 | if n.Parent != nil { 97 | out = append(out, n.Parent.AllFlags(hide)...) 98 | } 99 | group := []*Flag{} 100 | for _, flag := range n.Flags { 101 | if !hide || !flag.Hidden { 102 | flag.Active = true 103 | group = append(group, flag) 104 | } 105 | } 106 | if len(group) > 0 { 107 | out = append(out, group) 108 | } 109 | return 110 | } 111 | 112 | // Leaves returns the leaf commands/arguments under Node. 113 | // 114 | // If "hidden" is true hidden leaves will be omitted. 115 | func (n *Node) Leaves(hide bool) (out []*Node) { 116 | _ = Visit(n, func(nd Visitable, next Next) error { 117 | if nd == n { 118 | return next(nil) 119 | } 120 | if node, ok := nd.(*Node); ok { 121 | if hide && node.Hidden { 122 | return nil 123 | } 124 | if len(node.Children) == 0 && node.Type != ApplicationNode { 125 | out = append(out, node) 126 | } 127 | } 128 | return next(nil) 129 | }) 130 | return 131 | } 132 | 133 | // Depth of the command from the application root. 134 | func (n *Node) Depth() int { 135 | depth := 0 136 | p := n.Parent 137 | for p != nil && p.Type != ApplicationNode { 138 | depth++ 139 | p = p.Parent 140 | } 141 | return depth 142 | } 143 | 144 | // Summary help string for the node (not including application name). 145 | func (n *Node) Summary() string { 146 | summary := n.Path() 147 | if flags := n.FlagSummary(true); flags != "" { 148 | summary += " " + flags 149 | } 150 | args := []string{} 151 | optional := 0 152 | for _, arg := range n.Positional { 153 | argSummary := arg.Summary() 154 | if arg.Tag.Optional { 155 | optional++ 156 | argSummary = strings.TrimRight(argSummary, "]") 157 | } 158 | args = append(args, argSummary) 159 | } 160 | if len(args) != 0 { 161 | summary += " " + strings.Join(args, " ") + strings.Repeat("]", optional) 162 | } else if len(n.Children) > 0 { 163 | summary += " " 164 | } 165 | allFlags := n.Flags 166 | if n.Parent != nil { 167 | allFlags = append(allFlags, n.Parent.Flags...) 168 | } 169 | for _, flag := range allFlags { 170 | if _, ok := flag.Target.Interface().(helpFlag); ok { 171 | continue 172 | } 173 | if !flag.Required { 174 | summary += " [flags]" 175 | break 176 | } 177 | } 178 | return summary 179 | } 180 | 181 | // FlagSummary for the node. 182 | func (n *Node) FlagSummary(hide bool) string { 183 | required := []string{} 184 | count := 0 185 | for _, group := range n.AllFlags(hide) { 186 | for _, flag := range group { 187 | count++ 188 | if flag.Required { 189 | required = append(required, flag.Summary()) 190 | } 191 | } 192 | } 193 | return strings.Join(required, " ") 194 | } 195 | 196 | // FullPath is like Path() but includes the Application root node. 197 | func (n *Node) FullPath() string { 198 | root := n 199 | for root.Parent != nil { 200 | root = root.Parent 201 | } 202 | return strings.TrimSpace(root.Name + " " + n.Path()) 203 | } 204 | 205 | // Vars returns the combined Vars defined by all ancestors of this Node. 206 | func (n *Node) Vars() Vars { 207 | if n == nil { 208 | return Vars{} 209 | } 210 | return n.Parent.Vars().CloneWith(n.Tag.Vars) 211 | } 212 | 213 | // Path through ancestors to this Node. 214 | func (n *Node) Path() (out string) { 215 | if n.Parent != nil { 216 | out += " " + n.Parent.Path() 217 | } 218 | switch n.Type { 219 | case CommandNode: 220 | out += " " + n.Name 221 | if len(n.Aliases) > 0 { 222 | out += fmt.Sprintf(" (%s)", strings.Join(n.Aliases, ",")) 223 | } 224 | case ArgumentNode: 225 | out += " " + "<" + n.Name + ">" 226 | default: 227 | } 228 | return strings.TrimSpace(out) 229 | } 230 | 231 | // ClosestGroup finds the first non-nil group in this node and its ancestors. 232 | func (n *Node) ClosestGroup() *Group { 233 | switch { 234 | case n.Group != nil: 235 | return n.Group 236 | case n.Parent != nil: 237 | return n.Parent.ClosestGroup() 238 | default: 239 | return nil 240 | } 241 | } 242 | 243 | // A Value is either a flag or a variable positional argument. 244 | type Value struct { 245 | Flag *Flag // Nil if positional argument. 246 | Name string 247 | Help string 248 | OrigHelp string // Original help string, without interpolated variables. 249 | HasDefault bool 250 | Default string 251 | DefaultValue reflect.Value 252 | Enum string 253 | Mapper Mapper 254 | Tag *Tag 255 | Target reflect.Value 256 | Required bool 257 | Set bool // Set to true when this value is set through some mechanism. 258 | Format string // Formatting directive, if applicable. 259 | Position int // Position (for positional arguments). 260 | Passthrough bool // Deprecated: Use PassthroughMode instead. Set to true to stop flag parsing when encountered. 261 | PassthroughMode PassthroughMode // 262 | Active bool // Denotes the value is part of an active branch in the CLI. 263 | } 264 | 265 | // EnumMap returns a map of the enums in this value. 266 | func (v *Value) EnumMap() map[string]bool { 267 | parts := strings.Split(v.Enum, ",") 268 | out := make(map[string]bool, len(parts)) 269 | for _, part := range parts { 270 | out[strings.TrimSpace(part)] = true 271 | } 272 | return out 273 | } 274 | 275 | // EnumSlice returns a slice of the enums in this value. 276 | func (v *Value) EnumSlice() []string { 277 | parts := strings.Split(v.Enum, ",") 278 | out := make([]string, len(parts)) 279 | for i, part := range parts { 280 | out[i] = strings.TrimSpace(part) 281 | } 282 | return out 283 | } 284 | 285 | // ShortSummary returns a human-readable summary of the value, not including any placeholders/defaults. 286 | func (v *Value) ShortSummary() string { 287 | if v.Flag != nil { 288 | return fmt.Sprintf("--%s", v.Name) 289 | } 290 | argText := "<" + v.Name + ">" 291 | if v.IsCumulative() { 292 | argText += " ..." 293 | } 294 | if !v.Required { 295 | argText = "[" + argText + "]" 296 | } 297 | return argText 298 | } 299 | 300 | // Summary returns a human-readable summary of the value. 301 | func (v *Value) Summary() string { 302 | if v.Flag != nil { 303 | if v.IsBool() { 304 | return fmt.Sprintf("--%s", v.Name) 305 | } 306 | return fmt.Sprintf("--%s=%s", v.Name, v.Flag.FormatPlaceHolder()) 307 | } 308 | argText := "<" + v.Name + ">" 309 | if v.IsCumulative() { 310 | argText += " ..." 311 | } 312 | if !v.Required { 313 | argText = "[" + argText + "]" 314 | } 315 | return argText 316 | } 317 | 318 | // IsCumulative returns true if the type can be accumulated into. 319 | func (v *Value) IsCumulative() bool { 320 | return v.IsSlice() || v.IsMap() 321 | } 322 | 323 | // IsSlice returns true if the value is a slice. 324 | func (v *Value) IsSlice() bool { 325 | return v.Target.Type().Name() == "" && v.Target.Kind() == reflect.Slice 326 | } 327 | 328 | // IsMap returns true if the value is a map. 329 | func (v *Value) IsMap() bool { 330 | return v.Target.Kind() == reflect.Map 331 | } 332 | 333 | // IsBool returns true if the underlying value is a boolean. 334 | func (v *Value) IsBool() bool { 335 | if m, ok := v.Mapper.(BoolMapperExt); ok && m.IsBoolFromValue(v.Target) { 336 | return true 337 | } 338 | if m, ok := v.Mapper.(BoolMapper); ok && m.IsBool() { 339 | return true 340 | } 341 | return v.Target.Kind() == reflect.Bool 342 | } 343 | 344 | // IsCounter returns true if the value is a counter. 345 | func (v *Value) IsCounter() bool { 346 | return v.Tag.Type == "counter" 347 | } 348 | 349 | // Parse tokens into value, parse, and validate, but do not write to the field. 350 | func (v *Value) Parse(scan *Scanner, target reflect.Value) (err error) { 351 | if target.Kind() == reflect.Ptr && target.IsNil() { 352 | target.Set(reflect.New(target.Type().Elem())) 353 | } 354 | err = v.Mapper.Decode(&DecodeContext{Value: v, Scan: scan}, target) 355 | if err != nil { 356 | return fmt.Errorf("%s: %w", v.ShortSummary(), err) 357 | } 358 | v.Set = true 359 | return nil 360 | } 361 | 362 | // Apply value to field. 363 | func (v *Value) Apply(value reflect.Value) { 364 | v.Target.Set(value) 365 | v.Set = true 366 | } 367 | 368 | // ApplyDefault value to field if it is not already set. 369 | func (v *Value) ApplyDefault() error { 370 | if reflectValueIsZero(v.Target) { 371 | return v.Reset() 372 | } 373 | v.Set = true 374 | return nil 375 | } 376 | 377 | // Reset this value to its default, either the zero value or the parsed result of its envar, 378 | // or its "default" tag. 379 | // 380 | // Does not include resolvers. 381 | func (v *Value) Reset() error { 382 | v.Target.Set(reflect.Zero(v.Target.Type())) 383 | if len(v.Tag.Envs) != 0 { 384 | for _, env := range v.Tag.Envs { 385 | envar, ok := os.LookupEnv(env) 386 | // Parse the first non-empty ENV in the list 387 | if ok { 388 | err := v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: envar}), v.Target) 389 | if err != nil { 390 | return fmt.Errorf("%s (from envar %s=%q)", err, env, envar) 391 | } 392 | return nil 393 | } 394 | } 395 | } 396 | if v.HasDefault { 397 | return v.Parse(ScanFromTokens(Token{Type: FlagValueToken, Value: v.Default}), v.Target) 398 | } 399 | return nil 400 | } 401 | 402 | func (*Value) node() {} 403 | 404 | // A Positional represents a non-branching command-line positional argument. 405 | type Positional = Value 406 | 407 | // A Flag represents a command-line flag. 408 | type Flag struct { 409 | *Value 410 | Group *Group // Logical grouping when displaying. May also be used by configuration loaders to group options logically. 411 | Xor []string 412 | And []string 413 | PlaceHolder string 414 | Envs []string 415 | Aliases []string 416 | Short rune 417 | Hidden bool 418 | Negated bool 419 | } 420 | 421 | func (f *Flag) String() string { 422 | out := "--" + f.Name 423 | if f.Short != 0 { 424 | out = fmt.Sprintf("-%c, %s", f.Short, out) 425 | } 426 | if !f.IsBool() && !f.IsCounter() { 427 | out += "=" + f.FormatPlaceHolder() 428 | } 429 | return out 430 | } 431 | 432 | // FormatPlaceHolder formats the placeholder string for a Flag. 433 | func (f *Flag) FormatPlaceHolder() string { 434 | placeholderHelper, ok := f.Value.Mapper.(PlaceHolderProvider) 435 | if ok { 436 | return placeholderHelper.PlaceHolder(f) 437 | } 438 | tail := "" 439 | if f.Value.IsSlice() && f.Value.Tag.Sep != -1 && f.Tag.Type == "" { 440 | tail += string(f.Value.Tag.Sep) + "..." 441 | } 442 | if f.PlaceHolder != "" { 443 | return f.PlaceHolder + tail 444 | } 445 | if f.HasDefault { 446 | if f.Value.Target.Kind() == reflect.String { 447 | return strconv.Quote(f.Default) + tail 448 | } 449 | return f.Default + tail 450 | } 451 | if f.Value.IsMap() { 452 | if f.Value.Tag.MapSep != -1 && f.Tag.Type == "" { 453 | tail = string(f.Value.Tag.MapSep) + "..." 454 | } 455 | return "KEY=VALUE" + tail 456 | } 457 | if f.Tag != nil && f.Tag.TypeName != "" { 458 | return strings.ToUpper(dashedString(f.Tag.TypeName)) + tail 459 | } 460 | return strings.ToUpper(f.Name) + tail 461 | } 462 | 463 | // Group holds metadata about a command or flag group used when printing help. 464 | type Group struct { 465 | // Key is the `group` field tag value used to identify this group. 466 | Key string 467 | // Title is displayed above the grouped items. 468 | Title string 469 | // Description is optional and displayed under the Title when non empty. 470 | // It can be used to introduce the group's purpose to the user. 471 | Description string 472 | } 473 | 474 | // This is directly from the Go 1.13 source code. 475 | func reflectValueIsZero(v reflect.Value) bool { 476 | switch v.Kind() { 477 | case reflect.Bool: 478 | return !v.Bool() 479 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 480 | return v.Int() == 0 481 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 482 | return v.Uint() == 0 483 | case reflect.Float32, reflect.Float64: 484 | return math.Float64bits(v.Float()) == 0 485 | case reflect.Complex64, reflect.Complex128: 486 | c := v.Complex() 487 | return math.Float64bits(real(c)) == 0 && math.Float64bits(imag(c)) == 0 488 | case reflect.Array: 489 | for i := 0; i < v.Len(); i++ { 490 | if !reflectValueIsZero(v.Index(i)) { 491 | return false 492 | } 493 | } 494 | return true 495 | case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer: 496 | return v.IsNil() 497 | case reflect.String: 498 | return v.Len() == 0 499 | case reflect.Struct: 500 | for i := 0; i < v.NumField(); i++ { 501 | if !reflectValueIsZero(v.Field(i)) { 502 | return false 503 | } 504 | } 505 | return true 506 | default: 507 | // This should never happens, but will act as a safeguard for 508 | // later, as a default value doesn't makes sense here. 509 | panic(&reflect.ValueError{ 510 | Method: "reflect.Value.IsZero", 511 | Kind: v.Kind(), 512 | }) 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /model_test.go: -------------------------------------------------------------------------------- 1 | package kong_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | ) 9 | 10 | func TestModelApplicationCommands(t *testing.T) { 11 | var cli struct { 12 | One struct { 13 | Two struct { 14 | } `kong:"cmd"` 15 | Three struct { 16 | Four struct { 17 | Four string `kong:"arg"` 18 | } `kong:"arg"` 19 | } `kong:"cmd"` 20 | } `kong:"cmd"` 21 | } 22 | p := mustNew(t, &cli) 23 | actual := []string{} 24 | for _, cmd := range p.Model.Leaves(false) { 25 | actual = append(actual, cmd.Path()) 26 | } 27 | assert.Equal(t, []string{"one two", "one three "}, actual) 28 | } 29 | 30 | func TestFlagString(t *testing.T) { 31 | var cli struct { 32 | String string 33 | DefaultInt int `default:"42"` 34 | DefaultStr string `default:"hello"` 35 | Placeholder string `placeholder:"world"` 36 | DefaultPlaceholder string `default:"hello" placeholder:"world"` 37 | SliceSep []string 38 | SliceNoSep []string `sep:"none"` 39 | SliceDefault []string `default:"hello"` 40 | SlicePlaceholder []string `placeholder:"world"` 41 | SliceDefaultPlaceholder []string `default:"hello" placeholder:"world"` 42 | MapSep map[string]string 43 | MapNoSep map[string]string `mapsep:"none"` 44 | MapDefault map[string]string `mapsep:"none" default:"hello"` 45 | MapPlaceholder map[string]string `mapsep:"none" placeholder:"world"` 46 | Counter int `type:"counter"` 47 | } 48 | tests := map[string]string{ 49 | "help": "-h, --help", 50 | "string": "--string=STRING", 51 | "default-int": "--default-int=42", 52 | "default-str": `--default-str="hello"`, 53 | "placeholder": "--placeholder=world", 54 | "default-placeholder": "--default-placeholder=world", 55 | "slice-sep": "--slice-sep=SLICE-SEP,...", 56 | "slice-no-sep": "--slice-no-sep=SLICE-NO-SEP", 57 | "slice-default": "--slice-default=hello,...", 58 | "slice-placeholder": "--slice-placeholder=world,...", 59 | "slice-default-placeholder": "--slice-default-placeholder=world,...", 60 | "map-sep": "--map-sep=KEY=VALUE;...", 61 | "map-no-sep": "--map-no-sep=KEY=VALUE", 62 | "map-default": "--map-default=hello", 63 | "map-placeholder": "--map-placeholder=world", 64 | "counter": "--counter", 65 | } 66 | 67 | p := mustNew(t, &cli) 68 | for _, flag := range p.Model.Flags { 69 | want, ok := tests[flag.Name] 70 | assert.True(t, ok, "unknown flag name: %s", flag.Name) 71 | assert.Equal(t, want, flag.String()) 72 | } 73 | } 74 | 75 | func TestIgnoreHelpInUsage(t *testing.T) { 76 | var cli struct { 77 | One string `required:""` 78 | } 79 | 80 | k := mustNew(t, &cli) 81 | w := &bytes.Buffer{} 82 | k.Stdout = w 83 | k.Exit = func(code int) {} 84 | _, err := k.Parse([]string{"--help"}) 85 | assert.Error(t, err) 86 | assert.Equal(t, `Usage: test --one=STRING 87 | 88 | Flags: 89 | -h, --help Show context-sensitive help. 90 | --one=STRING 91 | `, w.String()) 92 | } 93 | -------------------------------------------------------------------------------- /negatable.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | // negatableDefault is a placeholder value for the Negatable tag to indicate 4 | // the negated flag is --no-. This is needed as at the time of 5 | // parsing a tag, the field's flag name is not yet known. 6 | const negatableDefault = "_" 7 | 8 | // negatableFlagName returns the name of the flag for a negatable field, or 9 | // an empty string if the field is not negatable. 10 | func negatableFlagName(name, negation string) string { 11 | switch negation { 12 | case "": 13 | return "" 14 | case negatableDefault: 15 | return "--no-" + name 16 | default: 17 | return "--" + negation 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/user" 9 | "path/filepath" 10 | "reflect" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | // An Option applies optional changes to the Kong application. 16 | type Option interface { 17 | Apply(k *Kong) error 18 | } 19 | 20 | // OptionFunc is function that adheres to the Option interface. 21 | type OptionFunc func(k *Kong) error 22 | 23 | func (o OptionFunc) Apply(k *Kong) error { return o(k) } //nolint: revive 24 | 25 | // Vars sets the variables to use for interpolation into help strings and default values. 26 | // 27 | // See README for details. 28 | type Vars map[string]string 29 | 30 | // Apply lets Vars act as an Option. 31 | func (v Vars) Apply(k *Kong) error { 32 | for key, value := range v { 33 | k.vars[key] = value 34 | } 35 | return nil 36 | } 37 | 38 | // CloneWith clones the current Vars and merges "vars" onto the clone. 39 | func (v Vars) CloneWith(vars Vars) Vars { 40 | out := make(Vars, len(v)+len(vars)) 41 | for key, value := range v { 42 | out[key] = value 43 | } 44 | for key, value := range vars { 45 | out[key] = value 46 | } 47 | return out 48 | } 49 | 50 | // Exit overrides the function used to terminate. This is useful for testing or interactive use. 51 | func Exit(exit func(int)) Option { 52 | return OptionFunc(func(k *Kong) error { 53 | k.Exit = exit 54 | return nil 55 | }) 56 | } 57 | 58 | // WithHyphenPrefixedParameters enables or disables hyphen-prefixed parameters. 59 | // 60 | // These are disabled by default. 61 | func WithHyphenPrefixedParameters(enable bool) Option { 62 | return OptionFunc(func(k *Kong) error { 63 | k.allowHyphenated = enable 64 | return nil 65 | }) 66 | } 67 | 68 | type embedded struct { 69 | strct any 70 | tags []string 71 | } 72 | 73 | // Embed a struct into the root of the CLI. 74 | // 75 | // "strct" must be a pointer to a structure. 76 | func Embed(strct any, tags ...string) Option { 77 | t := reflect.TypeOf(strct) 78 | if t.Kind() != reflect.Ptr || t.Elem().Kind() != reflect.Struct { 79 | panic("kong: Embed() must be called with a pointer to a struct") 80 | } 81 | return OptionFunc(func(k *Kong) error { 82 | k.embedded = append(k.embedded, embedded{strct, tags}) 83 | return nil 84 | }) 85 | } 86 | 87 | type dynamicCommand struct { 88 | name string 89 | help string 90 | group string 91 | tags []string 92 | cmd any 93 | } 94 | 95 | // DynamicCommand registers a dynamically constructed command with the root of the CLI. 96 | // 97 | // This is useful for command-line structures that are extensible via user-provided plugins. 98 | // 99 | // "tags" is a list of extra tag strings to parse, in the form :"". 100 | func DynamicCommand(name, help, group string, cmd any, tags ...string) Option { 101 | return OptionFunc(func(k *Kong) error { 102 | if run := getMethod(reflect.Indirect(reflect.ValueOf(cmd)), "Run"); !run.IsValid() { 103 | return fmt.Errorf("kong: DynamicCommand %q must be a type with a 'Run' method; got %T", name, cmd) 104 | } 105 | 106 | k.dynamicCommands = append(k.dynamicCommands, &dynamicCommand{ 107 | name: name, 108 | help: help, 109 | group: group, 110 | cmd: cmd, 111 | tags: tags, 112 | }) 113 | return nil 114 | }) 115 | } 116 | 117 | // NoDefaultHelp disables the default help flags. 118 | func NoDefaultHelp() Option { 119 | return OptionFunc(func(k *Kong) error { 120 | k.noDefaultHelp = true 121 | return nil 122 | }) 123 | } 124 | 125 | // PostBuild provides read/write access to kong.Kong after initial construction of the model is complete but before 126 | // parsing occurs. 127 | // 128 | // This is useful for, e.g., adding short options to flags, updating help, etc. 129 | func PostBuild(fn func(*Kong) error) Option { 130 | return OptionFunc(func(k *Kong) error { 131 | k.postBuildOptions = append(k.postBuildOptions, OptionFunc(fn)) 132 | return nil 133 | }) 134 | } 135 | 136 | // WithBeforeReset registers a hook to run before fields values are reset to their defaults 137 | // (as specified in the grammar) or to zero values. 138 | func WithBeforeReset(fn any) Option { 139 | return withHook("BeforeReset", fn) 140 | } 141 | 142 | // WithBeforeResolve registers a hook to run before resolvers are applied. 143 | func WithBeforeResolve(fn any) Option { 144 | return withHook("BeforeResolve", fn) 145 | } 146 | 147 | // WithBeforeApply registers a hook to run before command line arguments are applied to the grammar. 148 | func WithBeforeApply(fn any) Option { 149 | return withHook("BeforeApply", fn) 150 | } 151 | 152 | // WithAfterApply registers a hook to run after values are applied to the grammar and validated. 153 | func WithAfterApply(fn any) Option { 154 | return withHook("AfterApply", fn) 155 | } 156 | 157 | // withHook registers a named hook. 158 | func withHook(name string, fn any) Option { 159 | value := reflect.ValueOf(fn) 160 | if value.Kind() != reflect.Func { 161 | panic(fmt.Errorf("expected function, got %s", value.Type())) 162 | } 163 | 164 | return OptionFunc(func(k *Kong) error { 165 | k.hooks[name] = append(k.hooks[name], value) 166 | return nil 167 | }) 168 | } 169 | 170 | // Name overrides the application name. 171 | func Name(name string) Option { 172 | return PostBuild(func(k *Kong) error { 173 | k.Model.Name = name 174 | return nil 175 | }) 176 | } 177 | 178 | // Description sets the application description. 179 | func Description(description string) Option { 180 | return PostBuild(func(k *Kong) error { 181 | k.Model.Help = description 182 | return nil 183 | }) 184 | } 185 | 186 | // TypeMapper registers a mapper to a type. 187 | func TypeMapper(typ reflect.Type, mapper Mapper) Option { 188 | return OptionFunc(func(k *Kong) error { 189 | k.registry.RegisterType(typ, mapper) 190 | return nil 191 | }) 192 | } 193 | 194 | // KindMapper registers a mapper to a kind. 195 | func KindMapper(kind reflect.Kind, mapper Mapper) Option { 196 | return OptionFunc(func(k *Kong) error { 197 | k.registry.RegisterKind(kind, mapper) 198 | return nil 199 | }) 200 | } 201 | 202 | // ValueMapper registers a mapper to a field value. 203 | func ValueMapper(ptr any, mapper Mapper) Option { 204 | return OptionFunc(func(k *Kong) error { 205 | k.registry.RegisterValue(ptr, mapper) 206 | return nil 207 | }) 208 | } 209 | 210 | // NamedMapper registers a mapper to a name. 211 | func NamedMapper(name string, mapper Mapper) Option { 212 | return OptionFunc(func(k *Kong) error { 213 | k.registry.RegisterName(name, mapper) 214 | return nil 215 | }) 216 | } 217 | 218 | // Writers overrides the default writers. Useful for testing or interactive use. 219 | func Writers(stdout, stderr io.Writer) Option { 220 | return OptionFunc(func(k *Kong) error { 221 | k.Stdout = stdout 222 | k.Stderr = stderr 223 | return nil 224 | }) 225 | } 226 | 227 | // Bind binds values for hooks and Run() function arguments. 228 | // 229 | // Any arguments passed will be available to the receiving hook functions, but may be omitted. Additionally, *Kong and 230 | // the current *Context will also be made available. 231 | // 232 | // There are two hook points: 233 | // 234 | // BeforeApply(...) error 235 | // AfterApply(...) error 236 | // 237 | // Called before validation/assignment, and immediately after validation/assignment, respectively. 238 | func Bind(args ...any) Option { 239 | return OptionFunc(func(k *Kong) error { 240 | k.bindings.add(args...) 241 | return nil 242 | }) 243 | } 244 | 245 | // BindTo allows binding of implementations to interfaces. 246 | // 247 | // BindTo(impl, (*iface)(nil)) 248 | func BindTo(impl, iface any) Option { 249 | return OptionFunc(func(k *Kong) error { 250 | k.bindings.addTo(impl, iface) 251 | return nil 252 | }) 253 | } 254 | 255 | // BindToProvider binds an injected value to a provider function. 256 | // 257 | // The provider function must have one of the following signatures: 258 | // 259 | // func(...) (T, error) 260 | // func(...) T 261 | // 262 | // Where arguments to the function are injected by Kong. 263 | // 264 | // This is useful when the Run() function of different commands require different values that may 265 | // not all be initialisable from the main() function. 266 | func BindToProvider(provider any) Option { 267 | return OptionFunc(func(k *Kong) error { 268 | return k.bindings.addProvider(provider, false /* singleton */) 269 | }) 270 | } 271 | 272 | // BindSingletonProvider binds an injected value to a provider function. 273 | // The provider function must have the signature: 274 | // 275 | // func(...) (T, error) 276 | // func(...) T 277 | // 278 | // Unlike [BindToProvider], the provider function will only be called 279 | // at most once, and the result will be cached and reused 280 | // across multiple recipients of the injected value. 281 | func BindSingletonProvider(provider any) Option { 282 | return OptionFunc(func(k *Kong) error { 283 | return k.bindings.addProvider(provider, true /* singleton */) 284 | }) 285 | } 286 | 287 | // Help printer to use. 288 | func Help(help HelpPrinter) Option { 289 | return OptionFunc(func(k *Kong) error { 290 | k.help = help 291 | return nil 292 | }) 293 | } 294 | 295 | // ShortHelp configures the short usage message. 296 | // 297 | // It should be used together with kong.ShortUsageOnError() to display a 298 | // custom short usage message on errors. 299 | func ShortHelp(shortHelp HelpPrinter) Option { 300 | return OptionFunc(func(k *Kong) error { 301 | k.shortHelp = shortHelp 302 | return nil 303 | }) 304 | } 305 | 306 | // HelpFormatter configures how the help text is formatted. 307 | // 308 | // Deprecated: Use ValueFormatter() instead. 309 | func HelpFormatter(helpFormatter HelpValueFormatter) Option { 310 | return OptionFunc(func(k *Kong) error { 311 | k.helpFormatter = helpFormatter 312 | return nil 313 | }) 314 | } 315 | 316 | // ValueFormatter configures how the help text is formatted. 317 | func ValueFormatter(helpFormatter HelpValueFormatter) Option { 318 | return OptionFunc(func(k *Kong) error { 319 | k.helpFormatter = helpFormatter 320 | return nil 321 | }) 322 | } 323 | 324 | // ConfigureHelp sets the HelpOptions to use for printing help. 325 | func ConfigureHelp(options HelpOptions) Option { 326 | return OptionFunc(func(k *Kong) error { 327 | k.helpOptions = options 328 | return nil 329 | }) 330 | } 331 | 332 | // AutoGroup automatically assigns groups to flags. 333 | func AutoGroup(format func(parent Visitable, flag *Flag) *Group) Option { 334 | return PostBuild(func(kong *Kong) error { 335 | parents := []Visitable{kong.Model} 336 | return Visit(kong.Model, func(node Visitable, next Next) error { 337 | if flag, ok := node.(*Flag); ok && flag.Group == nil { 338 | flag.Group = format(parents[len(parents)-1], flag) 339 | } 340 | parents = append(parents, node) 341 | defer func() { parents = parents[:len(parents)-1] }() 342 | return next(nil) 343 | }) 344 | }) 345 | } 346 | 347 | // Groups associates `group` field tags with group metadata. 348 | // 349 | // This option is used to simplify Kong tags while providing 350 | // rich group information such as title and optional description. 351 | // 352 | // Each key in the "groups" map corresponds to the value of a 353 | // `group` Kong tag, while the first line of the value will be 354 | // the title, and subsequent lines if any will be the description of 355 | // the group. 356 | // 357 | // See also ExplicitGroups for a more structured alternative. 358 | type Groups map[string]string 359 | 360 | func (g Groups) Apply(k *Kong) error { //nolint: revive 361 | for key, info := range g { 362 | lines := strings.Split(info, "\n") 363 | title := strings.TrimSpace(lines[0]) 364 | description := "" 365 | if len(lines) > 1 { 366 | description = strings.TrimSpace(strings.Join(lines[1:], "\n")) 367 | } 368 | k.groups = append(k.groups, Group{ 369 | Key: key, 370 | Title: title, 371 | Description: description, 372 | }) 373 | } 374 | return nil 375 | } 376 | 377 | // ExplicitGroups associates `group` field tags with their metadata. 378 | // 379 | // It can be used to provide a title or header to a command or flag group. 380 | func ExplicitGroups(groups []Group) Option { 381 | return OptionFunc(func(k *Kong) error { 382 | k.groups = groups 383 | return nil 384 | }) 385 | } 386 | 387 | // UsageOnError configures Kong to display context-sensitive usage if FatalIfErrorf is called with an error. 388 | func UsageOnError() Option { 389 | return OptionFunc(func(k *Kong) error { 390 | k.usageOnError = fullUsage 391 | return nil 392 | }) 393 | } 394 | 395 | // ShortUsageOnError configures Kong to display context-sensitive short 396 | // usage if FatalIfErrorf is called with an error. The default short 397 | // usage message can be overridden with kong.ShortHelp(...). 398 | func ShortUsageOnError() Option { 399 | return OptionFunc(func(k *Kong) error { 400 | k.usageOnError = shortUsage 401 | return nil 402 | }) 403 | } 404 | 405 | // ClearResolvers clears all existing resolvers. 406 | func ClearResolvers() Option { 407 | return OptionFunc(func(k *Kong) error { 408 | k.resolvers = nil 409 | return nil 410 | }) 411 | } 412 | 413 | // Resolvers registers flag resolvers. 414 | func Resolvers(resolvers ...Resolver) Option { 415 | return OptionFunc(func(k *Kong) error { 416 | k.resolvers = append(k.resolvers, resolvers...) 417 | return nil 418 | }) 419 | } 420 | 421 | // IgnoreFields will cause kong.New() to skip field names that match any 422 | // of the provided regex patterns. This is useful if you are not able to add a 423 | // kong="-" struct tag to a struct/element before the call to New. 424 | // 425 | // Example: When referencing protoc generated structs, you will likely want to 426 | // ignore/skip XXX_* fields. 427 | func IgnoreFields(regexes ...string) Option { 428 | return OptionFunc(func(k *Kong) error { 429 | for _, r := range regexes { 430 | if r == "" { 431 | return errors.New("regex input cannot be empty") 432 | } 433 | 434 | re, err := regexp.Compile(r) 435 | if err != nil { 436 | return fmt.Errorf("unable to compile regex: %v", err) 437 | } 438 | 439 | k.ignoreFields = append(k.ignoreFields, re) 440 | } 441 | 442 | return nil 443 | }) 444 | } 445 | 446 | // ConfigurationLoader is a function that builds a resolver from a file. 447 | type ConfigurationLoader func(r io.Reader) (Resolver, error) 448 | 449 | // Configuration provides Kong with support for loading defaults from a set of configuration files. 450 | // 451 | // Paths will be opened in order, and "loader" will be used to provide a Resolver which is registered with Kong. 452 | // 453 | // Note: The JSON function is a ConfigurationLoader. 454 | // 455 | // ~ and variable expansion will occur on the provided paths. 456 | func Configuration(loader ConfigurationLoader, paths ...string) Option { 457 | return OptionFunc(func(k *Kong) error { 458 | k.loader = loader 459 | for _, path := range paths { 460 | f, err := os.Open(ExpandPath(path)) 461 | if err != nil { 462 | if os.IsNotExist(err) || os.IsPermission(err) { 463 | continue 464 | } 465 | 466 | return err 467 | } 468 | f.Close() 469 | 470 | resolver, err := k.LoadConfig(path) 471 | if err != nil { 472 | return fmt.Errorf("%s: %v", path, err) 473 | } 474 | if resolver != nil { 475 | k.resolvers = append(k.resolvers, resolver) 476 | } 477 | } 478 | return nil 479 | }) 480 | } 481 | 482 | // ExpandPath is a helper function to expand a relative or home-relative path to an absolute path. 483 | // 484 | // eg. ~/.someconf -> /home/alec/.someconf 485 | func ExpandPath(path string) string { 486 | if filepath.IsAbs(path) { 487 | return path 488 | } 489 | if strings.HasPrefix(path, "~/") { 490 | user, err := user.Current() 491 | if err != nil { 492 | return path 493 | } 494 | return filepath.Join(user.HomeDir, path[2:]) 495 | } 496 | abspath, err := filepath.Abs(path) 497 | if err != nil { 498 | return path 499 | } 500 | return abspath 501 | } 502 | 503 | func siftStrings(ss []string, filter func(s string) bool) []string { 504 | i := 0 505 | ss = append([]string(nil), ss...) 506 | for _, s := range ss { 507 | if filter(s) { 508 | ss[i] = s 509 | i++ 510 | } 511 | } 512 | return ss[0:i] 513 | } 514 | 515 | // DefaultEnvars option inits environment names for flags. 516 | // The name will not generate if tag "env" is "-". 517 | // Predefined environment variables are skipped. 518 | // 519 | // For example: 520 | // 521 | // --some.value -> PREFIX_SOME_VALUE 522 | func DefaultEnvars(prefix string) Option { 523 | processFlag := func(flag *Flag) { 524 | switch env := flag.Envs; { 525 | case flag.Name == "help": 526 | return 527 | case len(env) == 1 && env[0] == "-": 528 | flag.Envs = nil 529 | return 530 | case len(env) > 0: 531 | return 532 | } 533 | replacer := strings.NewReplacer("-", "_", ".", "_") 534 | names := append([]string{prefix}, camelCase(replacer.Replace(flag.Name))...) 535 | names = siftStrings(names, func(s string) bool { return !(s == "_" || strings.TrimSpace(s) == "") }) 536 | name := strings.ToUpper(strings.Join(names, "_")) 537 | flag.Envs = append(flag.Envs, name) 538 | flag.Value.Tag.Envs = append(flag.Value.Tag.Envs, name) 539 | } 540 | 541 | var processNode func(node *Node) 542 | processNode = func(node *Node) { 543 | for _, flag := range node.Flags { 544 | processFlag(flag) 545 | } 546 | for _, node := range node.Children { 547 | processNode(node) 548 | } 549 | } 550 | 551 | return PostBuild(func(k *Kong) error { 552 | processNode(k.Model.Node) 553 | return nil 554 | }) 555 | } 556 | 557 | // FlagNamer allows you to override the default kebab-case automated flag name generation. 558 | func FlagNamer(namer func(fieldName string) string) Option { 559 | return OptionFunc(func(k *Kong) error { 560 | k.flagNamer = namer 561 | return nil 562 | }) 563 | } 564 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | ) 10 | 11 | func TestOptions(t *testing.T) { 12 | var cli struct{} 13 | p, err := New(&cli, Name("name"), Description("description"), Writers(nil, nil), Exit(nil)) 14 | assert.NoError(t, err) 15 | assert.Equal(t, "name", p.Model.Name) 16 | assert.Equal(t, "description", p.Model.Help) 17 | assert.Zero(t, p.Stdout) 18 | assert.Zero(t, p.Stderr) 19 | assert.Zero(t, p.Exit) 20 | } 21 | 22 | type impl string 23 | 24 | func (impl) Method() {} 25 | 26 | func TestBindTo(t *testing.T) { 27 | type iface interface { 28 | Method() 29 | } 30 | 31 | saw := "" 32 | method := func(i iface) error { 33 | saw = string(i.(impl)) //nolint 34 | return nil 35 | } 36 | 37 | var cli struct{} 38 | 39 | p, err := New(&cli, BindTo(impl("foo"), (*iface)(nil))) 40 | assert.NoError(t, err) 41 | err = callFunction(reflect.ValueOf(method), p.bindings) 42 | assert.NoError(t, err) 43 | assert.Equal(t, "foo", saw) 44 | } 45 | 46 | func TestInvalidCallback(t *testing.T) { 47 | type iface interface { 48 | Method() 49 | } 50 | 51 | saw := "" 52 | method := func(i iface) string { 53 | saw = string(i.(impl)) //nolint 54 | return saw 55 | } 56 | 57 | var cli struct{} 58 | 59 | p, err := New(&cli, BindTo(impl("foo"), (*iface)(nil))) 60 | assert.NoError(t, err) 61 | err = callFunction(reflect.ValueOf(method), p.bindings) 62 | assert.EqualError(t, err, `return value of func(kong.iface) string must implement "error"`) 63 | } 64 | 65 | type zrror struct{} 66 | 67 | func (*zrror) Error() string { 68 | return "error" 69 | } 70 | 71 | func TestCallbackCustomError(t *testing.T) { 72 | type iface interface { 73 | Method() 74 | } 75 | 76 | saw := "" 77 | method := func(i iface) *zrror { 78 | saw = string(i.(impl)) //nolint 79 | return nil 80 | } 81 | 82 | var cli struct{} 83 | 84 | p, err := New(&cli, BindTo(impl("foo"), (*iface)(nil))) 85 | assert.NoError(t, err) 86 | err = callFunction(reflect.ValueOf(method), p.bindings) 87 | assert.NoError(t, err) 88 | assert.Equal(t, "foo", saw) 89 | } 90 | 91 | type bindToProviderCLI struct { 92 | Filled bool `default:"true"` 93 | Called bool 94 | Cmd bindToProviderCmd `cmd:""` 95 | } 96 | 97 | type boundThing struct { 98 | Filled bool 99 | } 100 | 101 | type bindToProviderCmd struct{} 102 | 103 | func (*bindToProviderCmd) Run(cli *bindToProviderCLI, b *boundThing) error { 104 | cli.Called = true 105 | return nil 106 | } 107 | 108 | func TestBindToProvider(t *testing.T) { 109 | var cli bindToProviderCLI 110 | app, err := New(&cli, BindToProvider(func(cli *bindToProviderCLI) (*boundThing, error) { 111 | assert.True(t, cli.Filled, "CLI struct should have already been populated by Kong") 112 | return &boundThing{Filled: cli.Filled}, nil 113 | })) 114 | assert.NoError(t, err) 115 | ctx, err := app.Parse([]string{"cmd"}) 116 | assert.NoError(t, err) 117 | err = ctx.Run() 118 | assert.NoError(t, err) 119 | assert.True(t, cli.Called) 120 | } 121 | 122 | func TestBindSingletonProvider(t *testing.T) { 123 | type ( 124 | Connection struct{} 125 | ClientA struct{ conn *Connection } 126 | ClientB struct{ conn *Connection } 127 | ) 128 | 129 | var numConnections int 130 | newConnection := func() *Connection { 131 | numConnections++ 132 | return &Connection{} 133 | } 134 | 135 | var cli struct{} 136 | app, err := New(&cli, 137 | BindSingletonProvider(newConnection), 138 | BindToProvider(func(conn *Connection) *ClientA { 139 | return &ClientA{conn: conn} 140 | }), 141 | BindToProvider(func(conn *Connection) *ClientB { 142 | return &ClientB{conn: conn} 143 | }), 144 | ) 145 | assert.NoError(t, err) 146 | 147 | ctx, err := app.Parse([]string{}) 148 | assert.NoError(t, err) 149 | 150 | _, err = ctx.Call(func(a *ClientA, b *ClientB) { 151 | assert.NotZero(t, a.conn) 152 | assert.NotZero(t, b.conn) 153 | 154 | assert.Equal(t, 1, numConnections, "expected newConnection to be called only once") 155 | }) 156 | assert.NoError(t, err) 157 | } 158 | 159 | func TestFlagNamer(t *testing.T) { 160 | var cli struct { 161 | SomeFlag string 162 | } 163 | app, err := New(&cli, FlagNamer(strings.ToUpper)) 164 | assert.NoError(t, err) 165 | assert.Equal(t, "SOMEFLAG", app.Model.Flags[1].Name) 166 | } 167 | 168 | type npError string 169 | 170 | func (e npError) Error() string { 171 | return "ERROR: " + string(e) 172 | } 173 | 174 | func TestCallbackNonPointerError(t *testing.T) { 175 | method := func() error { 176 | return npError("failed") 177 | } 178 | 179 | var cli struct{} 180 | 181 | p, err := New(&cli) 182 | assert.NoError(t, err) 183 | err = callFunction(reflect.ValueOf(method), p.bindings) 184 | assert.EqualError(t, err, "ERROR: failed") 185 | } 186 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | extends: [ 4 | "config:recommended", 5 | ":semanticCommits", 6 | ":semanticCommitTypeAll(chore)", 7 | ":semanticCommitScope(deps)", 8 | "group:allNonMajor", 9 | "schedule:earlyMondays", // Run once a week. 10 | ], 11 | packageRules: [ 12 | { 13 | "matchPackageNames": ["golangci-lint"], 14 | "matchManagers": ["hermit"], 15 | "enabled": false 16 | }, 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /resolver.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | // A Resolver resolves a Flag value from an external source. 10 | type Resolver interface { 11 | // Validate configuration against Application. 12 | // 13 | // This can be used to validate that all provided configuration is valid within this application. 14 | Validate(app *Application) error 15 | 16 | // Resolve the value for a Flag. 17 | Resolve(context *Context, parent *Path, flag *Flag) (any, error) 18 | } 19 | 20 | // ResolverFunc is a convenience type for non-validating Resolvers. 21 | type ResolverFunc func(context *Context, parent *Path, flag *Flag) (any, error) 22 | 23 | var _ Resolver = ResolverFunc(nil) 24 | 25 | func (r ResolverFunc) Resolve(context *Context, parent *Path, flag *Flag) (any, error) { //nolint: revive 26 | return r(context, parent, flag) 27 | } 28 | func (r ResolverFunc) Validate(app *Application) error { return nil } //nolint: revive 29 | 30 | // JSON returns a Resolver that retrieves values from a JSON source. 31 | // 32 | // Flag names are used as JSON keys indirectly, by tring snake_case and camelCase variants. 33 | func JSON(r io.Reader) (Resolver, error) { 34 | values := map[string]any{} 35 | err := json.NewDecoder(r).Decode(&values) 36 | if err != nil { 37 | return nil, err 38 | } 39 | var f ResolverFunc = func(context *Context, parent *Path, flag *Flag) (any, error) { 40 | name := strings.ReplaceAll(flag.Name, "-", "_") 41 | snakeCaseName := snakeCase(flag.Name) 42 | raw, ok := values[name] 43 | if ok { 44 | return raw, nil 45 | } else if raw, ok = values[snakeCaseName]; ok { 46 | return raw, nil 47 | } 48 | raw = values 49 | for _, part := range strings.Split(name, ".") { 50 | if values, ok := raw.(map[string]any); ok { 51 | raw, ok = values[part] 52 | if !ok { 53 | return nil, nil 54 | } 55 | } else { 56 | return nil, nil 57 | } 58 | } 59 | return raw, nil 60 | } 61 | 62 | return f, nil 63 | } 64 | 65 | func snakeCase(name string) string { 66 | name = strings.Join(strings.Split(strings.Title(name), "-"), "") //nolint:staticcheck // Unicode punctuation not an issue 67 | return strings.ToLower(name[:1]) + name[1:] 68 | } 69 | -------------------------------------------------------------------------------- /resolver_test.go: -------------------------------------------------------------------------------- 1 | package kong_test 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/alecthomas/assert/v2" 10 | "github.com/alecthomas/kong" 11 | ) 12 | 13 | type envMap map[string]string 14 | 15 | func newEnvParser(t *testing.T, cli any, env envMap, options ...kong.Option) *kong.Kong { 16 | t.Helper() 17 | for name, value := range env { 18 | t.Setenv(name, value) 19 | } 20 | parser := mustNew(t, cli, options...) 21 | return parser 22 | } 23 | 24 | func TestEnvarsFlagBasic(t *testing.T) { 25 | var cli struct { 26 | String string `env:"KONG_STRING"` 27 | Slice []int `env:"KONG_SLICE"` 28 | Interp string `env:"${kongInterp}"` 29 | } 30 | kongInterpEnv := "KONG_INTERP" 31 | parser := newEnvParser(t, &cli, 32 | envMap{ 33 | "KONG_STRING": "bye", 34 | "KONG_SLICE": "5,2,9", 35 | "KONG_INTERP": "foo", 36 | }, 37 | kong.Vars{ 38 | "kongInterp": kongInterpEnv, 39 | }, 40 | ) 41 | 42 | _, err := parser.Parse([]string{}) 43 | assert.NoError(t, err) 44 | assert.Equal(t, "bye", cli.String) 45 | assert.Equal(t, []int{5, 2, 9}, cli.Slice) 46 | assert.Equal(t, "foo", cli.Interp) 47 | } 48 | 49 | func TestEnvarsFlagMultiple(t *testing.T) { 50 | var cli struct { 51 | FirstENVPresent string `env:"KONG_TEST1_1,KONG_TEST1_2"` 52 | SecondENVPresent string `env:"KONG_TEST2_1,KONG_TEST2_2"` 53 | } 54 | parser := newEnvParser(t, &cli, 55 | envMap{ 56 | "KONG_TEST1_1": "value1.1", 57 | "KONG_TEST1_2": "value1.2", 58 | "KONG_TEST2_2": "value2.2", 59 | }, 60 | ) 61 | 62 | _, err := parser.Parse([]string{}) 63 | assert.NoError(t, err) 64 | assert.Equal(t, "value1.1", cli.FirstENVPresent) 65 | assert.Equal(t, "value2.2", cli.SecondENVPresent) 66 | } 67 | 68 | func TestEnvarsFlagOverride(t *testing.T) { 69 | var cli struct { 70 | Flag string `env:"KONG_FLAG"` 71 | } 72 | parser := newEnvParser(t, &cli, envMap{"KONG_FLAG": "bye"}) 73 | 74 | _, err := parser.Parse([]string{"--flag=hello"}) 75 | assert.NoError(t, err) 76 | assert.Equal(t, "hello", cli.Flag) 77 | } 78 | 79 | func TestEnvarsTag(t *testing.T) { 80 | var cli struct { 81 | Slice []int `env:"KONG_NUMBERS"` 82 | } 83 | parser := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "5,2,9"}) 84 | 85 | _, err := parser.Parse([]string{}) 86 | assert.NoError(t, err) 87 | assert.Equal(t, []int{5, 2, 9}, cli.Slice) 88 | } 89 | 90 | func TestEnvarsEnvPrefix(t *testing.T) { 91 | type Anonymous struct { 92 | Slice []int `env:"NUMBERS"` 93 | } 94 | var cli struct { 95 | Anonymous `envprefix:"KONG_"` 96 | } 97 | parser := newEnvParser(t, &cli, envMap{"KONG_NUMBERS": "1,2,3"}) 98 | 99 | _, err := parser.Parse([]string{}) 100 | assert.NoError(t, err) 101 | assert.Equal(t, []int{1, 2, 3}, cli.Slice) 102 | } 103 | 104 | func TestEnvarsEnvPrefixMultiple(t *testing.T) { 105 | type Anonymous struct { 106 | Slice1 []int `env:"NUMBERS1_1,NUMBERS1_2"` 107 | Slice2 []int `env:"NUMBERS2_1,NUMBERS2_2"` 108 | } 109 | var cli struct { 110 | Anonymous `envprefix:"KONG_"` 111 | } 112 | parser := newEnvParser(t, &cli, envMap{"KONG_NUMBERS1_1": "1,2,3", "KONG_NUMBERS2_2": "5,6,7"}) 113 | 114 | _, err := parser.Parse([]string{}) 115 | assert.NoError(t, err) 116 | assert.Equal(t, []int{1, 2, 3}, cli.Slice1) 117 | assert.Equal(t, []int{5, 6, 7}, cli.Slice2) 118 | } 119 | 120 | func TestEnvarsNestedEnvPrefix(t *testing.T) { 121 | type NestedAnonymous struct { 122 | String string `env:"STRING"` 123 | } 124 | type Anonymous struct { 125 | NestedAnonymous `envprefix:"ANON_"` 126 | } 127 | var cli struct { 128 | Anonymous `envprefix:"KONG_"` 129 | } 130 | parser := newEnvParser(t, &cli, envMap{"KONG_ANON_STRING": "abc"}) 131 | 132 | _, err := parser.Parse([]string{}) 133 | assert.NoError(t, err) 134 | assert.Equal(t, "abc", cli.String) 135 | } 136 | 137 | func TestEnvarsWithDefault(t *testing.T) { 138 | var cli struct { 139 | Flag string `env:"KONG_FLAG" default:"default"` 140 | } 141 | parser := newEnvParser(t, &cli, envMap{}) 142 | 143 | _, err := parser.Parse(nil) 144 | assert.NoError(t, err) 145 | assert.Equal(t, "default", cli.Flag) 146 | 147 | parser = newEnvParser(t, &cli, envMap{"KONG_FLAG": "moo"}) 148 | _, err = parser.Parse(nil) 149 | assert.NoError(t, err) 150 | assert.Equal(t, "moo", cli.Flag) 151 | } 152 | 153 | func TestEnv(t *testing.T) { 154 | type Embed struct { 155 | Flag string 156 | } 157 | type Cli struct { 158 | One Embed `prefix:"one-" embed:""` 159 | Two Embed `prefix:"two." embed:""` 160 | Three Embed `prefix:"three_" embed:""` 161 | Four Embed `prefix:"four_" embed:""` 162 | Five bool 163 | Six bool `env:"-"` 164 | } 165 | 166 | var cli Cli 167 | 168 | expected := Cli{ 169 | One: Embed{Flag: "one"}, 170 | Two: Embed{Flag: "two"}, 171 | Three: Embed{Flag: "three"}, 172 | Four: Embed{Flag: "four"}, 173 | Five: true, 174 | } 175 | 176 | // With the prefix 177 | parser := newEnvParser(t, &cli, envMap{ 178 | "KONG_ONE_FLAG": "one", 179 | "KONG_TWO_FLAG": "two", 180 | "KONG_THREE_FLAG": "three", 181 | "KONG_FOUR_FLAG": "four", 182 | "KONG_FIVE": "true", 183 | "KONG_SIX": "true", 184 | }, kong.DefaultEnvars("KONG")) 185 | 186 | _, err := parser.Parse(nil) 187 | assert.NoError(t, err) 188 | assert.Equal(t, expected, cli) 189 | 190 | // Without the prefix 191 | parser = newEnvParser(t, &cli, envMap{ 192 | "ONE_FLAG": "one", 193 | "TWO_FLAG": "two", 194 | "THREE_FLAG": "three", 195 | "FOUR_FLAG": "four", 196 | "FIVE": "true", 197 | "SIX": "true", 198 | }, kong.DefaultEnvars("")) 199 | 200 | _, err = parser.Parse(nil) 201 | assert.NoError(t, err) 202 | assert.Equal(t, expected, cli) 203 | } 204 | 205 | func TestJSONBasic(t *testing.T) { 206 | type Embed struct { 207 | String string 208 | } 209 | 210 | var cli struct { 211 | String string 212 | Slice []int 213 | SliceWithCommas []string 214 | Bool bool 215 | 216 | One Embed `prefix:"one." embed:""` 217 | Two Embed `prefix:"two." embed:""` 218 | } 219 | 220 | json := `{ 221 | "string": "🍕", 222 | "slice": [5, 8], 223 | "bool": true, 224 | "sliceWithCommas": ["a,b", "c"], 225 | "one":{ 226 | "string": "one value" 227 | }, 228 | "two.string": "two value" 229 | }` 230 | 231 | r, err := kong.JSON(strings.NewReader(json)) 232 | assert.NoError(t, err) 233 | 234 | parser := mustNew(t, &cli, kong.Resolvers(r)) 235 | _, err = parser.Parse([]string{}) 236 | assert.NoError(t, err) 237 | assert.Equal(t, "🍕", cli.String) 238 | assert.Equal(t, []int{5, 8}, cli.Slice) 239 | assert.Equal(t, []string{"a,b", "c"}, cli.SliceWithCommas) 240 | assert.Equal(t, "one value", cli.One.String) 241 | assert.Equal(t, "two value", cli.Two.String) 242 | assert.True(t, cli.Bool) 243 | } 244 | 245 | type testUppercaseMapper struct{} 246 | 247 | func (testUppercaseMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error { 248 | var value string 249 | err := ctx.Scan.PopValueInto("lowercase", &value) 250 | if err != nil { 251 | return err 252 | } 253 | target.SetString(strings.ToUpper(value)) 254 | return nil 255 | } 256 | 257 | func TestResolversWithMappers(t *testing.T) { 258 | var cli struct { 259 | Flag string `env:"KONG_MOO" type:"upper"` 260 | } 261 | 262 | t.Setenv("KONG_MOO", "meow") 263 | 264 | parser := newEnvParser(t, &cli, 265 | envMap{"KONG_MOO": "meow"}, 266 | kong.NamedMapper("upper", testUppercaseMapper{}), 267 | ) 268 | _, err := parser.Parse([]string{}) 269 | assert.NoError(t, err) 270 | assert.Equal(t, "MEOW", cli.Flag) 271 | } 272 | 273 | func TestResolverWithBool(t *testing.T) { 274 | var cli struct { 275 | Bool bool 276 | } 277 | 278 | var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { 279 | if flag.Name == "bool" { 280 | return true, nil 281 | } 282 | return nil, nil 283 | } 284 | 285 | p := mustNew(t, &cli, kong.Resolvers(resolver)) 286 | 287 | _, err := p.Parse(nil) 288 | assert.NoError(t, err) 289 | assert.True(t, cli.Bool) 290 | } 291 | 292 | func TestLastResolverWins(t *testing.T) { 293 | var cli struct { 294 | Int []int 295 | } 296 | 297 | var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { 298 | if flag.Name == "int" { 299 | return 1, nil 300 | } 301 | return nil, nil 302 | } 303 | 304 | var second kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { 305 | if flag.Name == "int" { 306 | return 2, nil 307 | } 308 | return nil, nil 309 | } 310 | 311 | p := mustNew(t, &cli, kong.Resolvers(first, second)) 312 | _, err := p.Parse(nil) 313 | assert.NoError(t, err) 314 | assert.Equal(t, []int{2}, cli.Int) 315 | } 316 | 317 | func TestResolverSatisfiesRequired(t *testing.T) { 318 | var cli struct { 319 | Int int `required` 320 | } 321 | var resolver kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { 322 | if flag.Name == "int" { 323 | return 1, nil 324 | } 325 | return nil, nil 326 | } 327 | _, err := mustNew(t, &cli, kong.Resolvers(resolver)).Parse(nil) 328 | assert.NoError(t, err) 329 | assert.Equal(t, 1, cli.Int) 330 | } 331 | 332 | func TestResolverTriggersHooks(t *testing.T) { 333 | ctx := &hookContext{} 334 | 335 | var cli struct { 336 | Flag hookValue 337 | } 338 | 339 | var first kong.ResolverFunc = func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { 340 | if flag.Name == "flag" { 341 | return "one", nil 342 | } 343 | return nil, nil 344 | } 345 | 346 | _, err := mustNew(t, &cli, kong.Bind(ctx), kong.Resolvers(first)).Parse(nil) 347 | assert.NoError(t, err) 348 | 349 | assert.Equal(t, "one", string(cli.Flag)) 350 | assert.Equal(t, []string{"before:", "after:one"}, ctx.values) 351 | } 352 | 353 | type validatingResolver struct { 354 | err error 355 | } 356 | 357 | func (v *validatingResolver) Validate(app *kong.Application) error { return v.err } 358 | func (v *validatingResolver) Resolve(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) { 359 | return nil, nil 360 | } 361 | 362 | func TestValidatingResolverErrors(t *testing.T) { 363 | resolver := &validatingResolver{err: errors.New("invalid")} 364 | var cli struct{} 365 | _, err := mustNew(t, &cli, kong.Resolvers(resolver)).Parse(nil) 366 | assert.EqualError(t, err, "invalid") 367 | } 368 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // TokenType is the type of a token. 9 | type TokenType int 10 | 11 | // Token types. 12 | const ( 13 | UntypedToken TokenType = iota 14 | EOLToken 15 | FlagToken // -- 16 | FlagValueToken // = 17 | ShortFlagToken // -[ 19 | PositionalArgumentToken // 20 | ) 21 | 22 | func (t TokenType) String() string { 23 | switch t { 24 | case UntypedToken: 25 | return "untyped" 26 | case EOLToken: 27 | return "" 28 | case FlagToken: // -- 29 | return "long flag" 30 | case FlagValueToken: // = 31 | return "flag value" 32 | case ShortFlagToken: // -[ 35 | return "short flag remainder" 36 | case PositionalArgumentToken: // 37 | return "positional argument" 38 | } 39 | panic("unsupported type") 40 | } 41 | 42 | // Token created by Scanner. 43 | type Token struct { 44 | Value any 45 | Type TokenType 46 | } 47 | 48 | func (t Token) String() string { 49 | switch t.Type { 50 | case FlagToken: 51 | return fmt.Sprintf("--%v", t.Value) 52 | 53 | case ShortFlagToken: 54 | return fmt.Sprintf("-%v", t.Value) 55 | 56 | case EOLToken: 57 | return "EOL" 58 | 59 | default: 60 | return fmt.Sprintf("%v", t.Value) 61 | } 62 | } 63 | 64 | // IsEOL returns true if this Token is past the end of the line. 65 | func (t Token) IsEOL() bool { 66 | return t.Type == EOLToken 67 | } 68 | 69 | // IsAny returns true if the token's type is any of those provided. 70 | func (t TokenType) IsAny(types ...TokenType) bool { 71 | for _, typ := range types { 72 | if t == typ { 73 | return true 74 | } 75 | } 76 | return false 77 | } 78 | 79 | // InferredType tries to infer the type of a token. 80 | func (t Token) InferredType() TokenType { 81 | if t.Type != UntypedToken { 82 | return t.Type 83 | } 84 | if v, ok := t.Value.(string); ok { 85 | if strings.HasPrefix(v, "--") { //nolint: gocritic 86 | return FlagToken 87 | } else if v == "-" { 88 | return PositionalArgumentToken 89 | } else if strings.HasPrefix(v, "-") { 90 | return ShortFlagToken 91 | } 92 | } 93 | return t.Type 94 | } 95 | 96 | // IsValue returns true if token is usable as a parseable value. 97 | // 98 | // A parseable value is either a value typed token, or an untyped token NOT starting with a hyphen. 99 | func (t Token) IsValue() bool { 100 | tt := t.InferredType() 101 | return tt.IsAny(FlagValueToken, ShortFlagTailToken, PositionalArgumentToken) || 102 | (tt == UntypedToken && !strings.HasPrefix(t.String(), "-")) 103 | } 104 | 105 | // Scanner is a stack-based scanner over command-line tokens. 106 | // 107 | // Initially all tokens are untyped. As the parser consumes tokens it assigns types, splits tokens, and pushes them back 108 | // onto the stream. 109 | // 110 | // For example, the token "--foo=bar" will be split into the following by the parser: 111 | // 112 | // [{FlagToken, "foo"}, {FlagValueToken, "bar"}] 113 | type Scanner struct { 114 | allowHyphenated bool 115 | args []Token 116 | } 117 | 118 | // ScanAsType creates a new Scanner from args with the given type. 119 | func ScanAsType(ttype TokenType, args ...string) *Scanner { 120 | s := &Scanner{} 121 | for _, arg := range args { 122 | s.args = append(s.args, Token{Value: arg, Type: ttype}) 123 | } 124 | return s 125 | } 126 | 127 | // Scan creates a new Scanner from args with untyped tokens. 128 | func Scan(args ...string) *Scanner { 129 | return ScanAsType(UntypedToken, args...) 130 | } 131 | 132 | // ScanFromTokens creates a new Scanner from a slice of tokens. 133 | func ScanFromTokens(tokens ...Token) *Scanner { 134 | return &Scanner{args: tokens} 135 | } 136 | 137 | // AllowHyphenPrefixedParameters enables or disables hyphen-prefixed flag parameters on this Scanner. 138 | // 139 | // Disabled by default. 140 | func (s *Scanner) AllowHyphenPrefixedParameters(enable bool) *Scanner { 141 | s.allowHyphenated = enable 142 | return s 143 | } 144 | 145 | // Len returns the number of input arguments. 146 | func (s *Scanner) Len() int { 147 | return len(s.args) 148 | } 149 | 150 | // Pop the front token off the Scanner. 151 | func (s *Scanner) Pop() Token { 152 | if len(s.args) == 0 { 153 | return Token{Type: EOLToken} 154 | } 155 | arg := s.args[0] 156 | s.args = s.args[1:] 157 | return arg 158 | } 159 | 160 | type expectedError struct { 161 | context string 162 | token Token 163 | } 164 | 165 | func (e *expectedError) Error() string { 166 | return fmt.Sprintf("expected %s value but got %q (%s)", e.context, e.token, e.token.InferredType()) 167 | } 168 | 169 | // PopValue pops a value token, or returns an error. 170 | // 171 | // "context" is used to assist the user if the value can not be popped, eg. "expected value but got " 172 | func (s *Scanner) PopValue(context string) (Token, error) { 173 | t := s.Pop() 174 | if !s.allowHyphenated && !t.IsValue() { 175 | return t, &expectedError{context, t} 176 | } 177 | return t, nil 178 | } 179 | 180 | // PopValueInto pops a value token into target or returns an error. 181 | // 182 | // "context" is used to assist the user if the value can not be popped, eg. "expected value but got " 183 | func (s *Scanner) PopValueInto(context string, target any) error { 184 | t, err := s.PopValue(context) 185 | if err != nil { 186 | return err 187 | } 188 | return jsonTranscode(t.Value, target) 189 | } 190 | 191 | // PopWhile predicate returns true. 192 | func (s *Scanner) PopWhile(predicate func(Token) bool) (values []Token) { 193 | for predicate(s.Peek()) { 194 | values = append(values, s.Pop()) 195 | } 196 | return 197 | } 198 | 199 | // PopUntil predicate returns true. 200 | func (s *Scanner) PopUntil(predicate func(Token) bool) (values []Token) { 201 | for !predicate(s.Peek()) { 202 | values = append(values, s.Pop()) 203 | } 204 | return 205 | } 206 | 207 | // Peek at the next Token or return an EOLToken. 208 | func (s *Scanner) Peek() Token { 209 | if len(s.args) == 0 { 210 | return Token{Type: EOLToken} 211 | } 212 | return s.args[0] 213 | } 214 | 215 | // PeekAll remaining tokens 216 | func (s *Scanner) PeekAll() []Token { 217 | return s.args 218 | } 219 | 220 | // Push an untyped Token onto the front of the Scanner. 221 | func (s *Scanner) Push(arg any) *Scanner { 222 | s.PushToken(Token{Value: arg}) 223 | return s 224 | } 225 | 226 | // PushTyped pushes a typed token onto the front of the Scanner. 227 | func (s *Scanner) PushTyped(arg any, typ TokenType) *Scanner { 228 | s.PushToken(Token{Value: arg, Type: typ}) 229 | return s 230 | } 231 | 232 | // PushToken pushes a preconstructed Token onto the front of the Scanner. 233 | func (s *Scanner) PushToken(token Token) *Scanner { 234 | s.args = append([]Token{token}, s.args...) 235 | return s 236 | } 237 | -------------------------------------------------------------------------------- /scanner_test.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | ) 8 | 9 | func TestScannerTake(t *testing.T) { 10 | s := Scan("a", "b", "c", "-") 11 | assert.Equal(t, "a", s.Pop().Value) 12 | assert.Equal(t, "b", s.Pop().Value) 13 | assert.Equal(t, "c", s.Pop().Value) 14 | hyphen := s.Pop() 15 | assert.Equal(t, PositionalArgumentToken, hyphen.InferredType()) 16 | assert.Equal(t, EOLToken, s.Pop().Type) 17 | } 18 | 19 | func TestScannerPeek(t *testing.T) { 20 | s := Scan("a", "b", "c") 21 | assert.Equal(t, s.Peek().Value, "a") 22 | assert.Equal(t, s.Pop().Value, "a") 23 | assert.Equal(t, s.Peek().Value, "b") 24 | assert.Equal(t, s.Pop().Value, "b") 25 | assert.Equal(t, s.Peek().Value, "c") 26 | assert.Equal(t, s.Pop().Value, "c") 27 | assert.Equal(t, s.Peek().Type, EOLToken) 28 | } 29 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "unicode/utf8" 10 | ) 11 | 12 | // PassthroughMode indicates how parameters are passed through when "passthrough" is set. 13 | type PassthroughMode int 14 | 15 | const ( 16 | // PassThroughModeNone indicates passthrough mode is disabled. 17 | PassThroughModeNone PassthroughMode = iota 18 | // PassThroughModeAll indicates that all parameters, including flags, are passed through. It is the default. 19 | PassThroughModeAll 20 | // PassThroughModePartial will validate flags until the first positional argument is encountered, then pass through all remaining positional arguments. 21 | PassThroughModePartial 22 | ) 23 | 24 | // Tag represents the parsed state of Kong tags in a struct field tag. 25 | type Tag struct { 26 | Ignored bool // Field is ignored by Kong. ie. kong:"-" 27 | Cmd bool 28 | Arg bool 29 | Required bool 30 | Optional bool 31 | Name string 32 | Help string 33 | Type string 34 | TypeName string 35 | HasDefault bool 36 | Default string 37 | Format string 38 | PlaceHolder string 39 | Envs []string 40 | Short rune 41 | Hidden bool 42 | Sep rune 43 | MapSep rune 44 | Enum string 45 | Group string 46 | Xor []string 47 | And []string 48 | Vars Vars 49 | Prefix string // Optional prefix on anonymous structs. All sub-flags will have this prefix. 50 | EnvPrefix string 51 | XorPrefix string // Optional prefix on XOR/AND groups. 52 | Embed bool 53 | Aliases []string 54 | Negatable string 55 | Passthrough bool // Deprecated: use PassthroughMode instead. 56 | PassthroughMode PassthroughMode 57 | 58 | // Storage for all tag keys for arbitrary lookups. 59 | items map[string][]string 60 | } 61 | 62 | func (t *Tag) String() string { 63 | out := []string{} 64 | for key, list := range t.items { 65 | for _, value := range list { 66 | out = append(out, fmt.Sprintf("%s:%q", key, value)) 67 | } 68 | } 69 | return strings.Join(out, " ") 70 | } 71 | 72 | type tagChars struct { 73 | sep, quote, assign rune 74 | needsUnquote bool 75 | } 76 | 77 | var kongChars = tagChars{sep: ',', quote: '\'', assign: '=', needsUnquote: false} 78 | var bareChars = tagChars{sep: ' ', quote: '"', assign: ':', needsUnquote: true} 79 | 80 | //nolint:gocyclo 81 | func parseTagItems(tagString string, chr tagChars) (map[string][]string, error) { 82 | d := map[string][]string{} 83 | key := []rune{} 84 | value := []rune{} 85 | quotes := false 86 | inKey := true 87 | 88 | add := func() error { 89 | // Bare tags are quoted, therefore we need to unquote them in the same fashion reflect.Lookup() (implicitly) 90 | // unquotes "kong tags". 91 | s := string(value) 92 | 93 | if chr.needsUnquote && s != "" { 94 | if unquoted, err := strconv.Unquote(fmt.Sprintf(`"%s"`, s)); err == nil { 95 | s = unquoted 96 | } else { 97 | return fmt.Errorf("unquoting tag value `%s`: %w", s, err) 98 | } 99 | } 100 | 101 | d[string(key)] = append(d[string(key)], s) 102 | key = []rune{} 103 | value = []rune{} 104 | inKey = true 105 | 106 | return nil 107 | } 108 | 109 | runes := []rune(tagString) 110 | for idx := 0; idx < len(runes); idx++ { 111 | r := runes[idx] 112 | next := rune(0) 113 | eof := false 114 | if idx < len(runes)-1 { 115 | next = runes[idx+1] 116 | } else { 117 | eof = true 118 | } 119 | if !quotes && r == chr.sep { 120 | if err := add(); err != nil { 121 | return nil, err 122 | } 123 | 124 | continue 125 | } 126 | if r == chr.assign && inKey { 127 | inKey = false 128 | continue 129 | } 130 | if r == '\\' { 131 | if next == chr.quote { 132 | idx++ 133 | 134 | // We need to keep the backslashes, otherwise subsequent unquoting cannot work 135 | if chr.needsUnquote { 136 | value = append(value, r) 137 | } 138 | 139 | r = chr.quote 140 | } 141 | } else if r == chr.quote { 142 | if quotes { 143 | quotes = false 144 | if next == chr.sep || eof { 145 | continue 146 | } 147 | return nil, fmt.Errorf("%v has an unexpected char at pos %v", tagString, idx) 148 | } 149 | quotes = true 150 | continue 151 | } 152 | if inKey { 153 | key = append(key, r) 154 | } else { 155 | value = append(value, r) 156 | } 157 | } 158 | if quotes { 159 | return nil, fmt.Errorf("%v is not quoted properly", tagString) 160 | } 161 | 162 | if err := add(); err != nil { 163 | return nil, err 164 | } 165 | 166 | return d, nil 167 | } 168 | 169 | func getTagInfo(ft reflect.StructField) (string, tagChars) { 170 | s, ok := ft.Tag.Lookup("kong") 171 | if ok { 172 | return s, kongChars 173 | } 174 | 175 | return string(ft.Tag), bareChars 176 | } 177 | 178 | func newEmptyTag() *Tag { 179 | return &Tag{items: map[string][]string{}} 180 | } 181 | 182 | func tagSplitFn(r rune) bool { 183 | return r == ',' || r == ' ' 184 | } 185 | 186 | func parseTagString(s string) (*Tag, error) { 187 | items, err := parseTagItems(s, bareChars) 188 | if err != nil { 189 | return nil, err 190 | } 191 | t := &Tag{ 192 | items: items, 193 | } 194 | err = hydrateTag(t, nil) 195 | if err != nil { 196 | return nil, fmt.Errorf("%s: %s", s, err) 197 | } 198 | return t, nil 199 | } 200 | 201 | func parseTag(parent reflect.Value, ft reflect.StructField) (*Tag, error) { 202 | if ft.Tag.Get("kong") == "-" { 203 | t := newEmptyTag() 204 | t.Ignored = true 205 | return t, nil 206 | } 207 | items, err := parseTagItems(getTagInfo(ft)) 208 | if err != nil { 209 | return nil, err 210 | } 211 | t := &Tag{ 212 | items: items, 213 | } 214 | err = hydrateTag(t, ft.Type) 215 | if err != nil { 216 | return nil, failField(parent, ft, "%s", err) 217 | } 218 | return t, nil 219 | } 220 | 221 | func hydrateTag(t *Tag, typ reflect.Type) error { //nolint: gocyclo 222 | var typeName string 223 | var isBool bool 224 | var isBoolPtr bool 225 | if typ != nil { 226 | typeName = typ.Name() 227 | isBool = typ.Kind() == reflect.Bool 228 | isBoolPtr = typ.Kind() == reflect.Ptr && typ.Elem().Kind() == reflect.Bool 229 | } 230 | var err error 231 | t.Cmd = t.Has("cmd") 232 | t.Arg = t.Has("arg") 233 | required := t.Has("required") 234 | optional := t.Has("optional") 235 | if required && optional { 236 | return fmt.Errorf("can't specify both required and optional") 237 | } 238 | t.Required = required 239 | t.Optional = optional 240 | t.HasDefault = t.Has("default") 241 | t.Default = t.Get("default") 242 | // Arguments with defaults are always optional. 243 | if t.Arg && t.HasDefault { 244 | t.Optional = true 245 | } else if t.Arg && !optional { // Arguments are required unless explicitly made optional. 246 | t.Required = true 247 | } 248 | t.Name = t.Get("name") 249 | t.Help = t.Get("help") 250 | t.Type = t.Get("type") 251 | t.TypeName = typeName 252 | for _, env := range t.GetAll("env") { 253 | t.Envs = append(t.Envs, strings.FieldsFunc(env, tagSplitFn)...) 254 | } 255 | t.Short, err = t.GetRune("short") 256 | if err != nil && t.Get("short") != "" { 257 | return fmt.Errorf("invalid short flag name %q: %s", t.Get("short"), err) 258 | } 259 | t.Hidden = t.Has("hidden") 260 | t.Format = t.Get("format") 261 | t.Sep, _ = t.GetSep("sep", ',') 262 | t.MapSep, _ = t.GetSep("mapsep", ';') 263 | t.Group = t.Get("group") 264 | for _, xor := range t.GetAll("xor") { 265 | t.Xor = append(t.Xor, strings.FieldsFunc(xor, tagSplitFn)...) 266 | } 267 | for _, and := range t.GetAll("and") { 268 | t.And = append(t.And, strings.FieldsFunc(and, tagSplitFn)...) 269 | } 270 | t.Prefix = t.Get("prefix") 271 | t.EnvPrefix = t.Get("envprefix") 272 | t.XorPrefix = t.Get("xorprefix") 273 | t.Embed = t.Has("embed") 274 | if t.Has("negatable") { 275 | if !isBool && !isBoolPtr { 276 | return fmt.Errorf("negatable can only be set on booleans") 277 | } 278 | negatable := t.Get("negatable") 279 | if negatable == "" { 280 | negatable = negatableDefault // placeholder for default negation of --no- 281 | } 282 | t.Negatable = negatable 283 | } 284 | aliases := t.Get("aliases") 285 | if len(aliases) > 0 { 286 | t.Aliases = append(t.Aliases, strings.FieldsFunc(aliases, tagSplitFn)...) 287 | } 288 | t.Vars = Vars{} 289 | for _, set := range t.GetAll("set") { 290 | parts := strings.SplitN(set, "=", 2) 291 | if len(parts) == 0 { 292 | return fmt.Errorf("set should be in the form key=value but got %q", set) 293 | } 294 | t.Vars[parts[0]] = parts[1] 295 | } 296 | t.PlaceHolder = t.Get("placeholder") 297 | t.Enum = t.Get("enum") 298 | scalarType := typ == nil || !(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Ptr) 299 | if t.Enum != "" && !(t.Required || t.HasDefault) && scalarType { 300 | return fmt.Errorf("enum value is only valid if it is either required or has a valid default value") 301 | } 302 | passthrough := t.Has("passthrough") 303 | if passthrough && !t.Arg && !t.Cmd { 304 | return fmt.Errorf("passthrough only makes sense for positional arguments or commands") 305 | } 306 | t.Passthrough = passthrough 307 | if t.Passthrough { 308 | passthroughMode := t.Get("passthrough") 309 | switch passthroughMode { 310 | case "partial": 311 | t.PassthroughMode = PassThroughModePartial 312 | case "all", "": 313 | t.PassthroughMode = PassThroughModeAll 314 | default: 315 | return fmt.Errorf("invalid passthrough mode %q, must be one of 'partial' or 'all'", passthroughMode) 316 | } 317 | } 318 | return nil 319 | } 320 | 321 | // Has returns true if the tag contained the given key. 322 | func (t *Tag) Has(k string) bool { 323 | _, ok := t.items[k] 324 | return ok 325 | } 326 | 327 | // Get returns the value of the given tag. 328 | // 329 | // Note that this will return the empty string if the tag is missing. 330 | func (t *Tag) Get(k string) string { 331 | values := t.items[k] 332 | if len(values) == 0 { 333 | return "" 334 | } 335 | return values[0] 336 | } 337 | 338 | // GetAll returns all encountered values for a tag, in the case of multiple occurrences. 339 | func (t *Tag) GetAll(k string) []string { 340 | return t.items[k] 341 | } 342 | 343 | // GetBool returns true if the given tag looks like a boolean truth string. 344 | func (t *Tag) GetBool(k string) (bool, error) { 345 | return strconv.ParseBool(t.Get(k)) 346 | } 347 | 348 | // GetFloat parses the given tag as a float64. 349 | func (t *Tag) GetFloat(k string) (float64, error) { 350 | return strconv.ParseFloat(t.Get(k), 64) 351 | } 352 | 353 | // GetInt parses the given tag as an int64. 354 | func (t *Tag) GetInt(k string) (int64, error) { 355 | return strconv.ParseInt(t.Get(k), 10, 64) 356 | } 357 | 358 | // GetRune parses the given tag as a rune. 359 | func (t *Tag) GetRune(k string) (rune, error) { 360 | value := t.Get(k) 361 | r, size := utf8.DecodeRuneInString(value) 362 | if r == utf8.RuneError || size < len(value) { 363 | return 0, errors.New("invalid rune") 364 | } 365 | return r, nil 366 | } 367 | 368 | // GetSep parses the given tag as a rune separator, allowing for a default or none. 369 | // The separator is returned, or -1 if "none" is specified. If the tag value is an 370 | // invalid utf8 sequence, the default rune is returned as well as an error. If the 371 | // tag value is more than one rune, the first rune is returned as well as an error. 372 | func (t *Tag) GetSep(k string, dflt rune) (rune, error) { 373 | tv := t.Get(k) 374 | if tv == "none" { 375 | return -1, nil 376 | } else if tv == "" { 377 | return dflt, nil 378 | } 379 | r, size := utf8.DecodeRuneInString(tv) 380 | if r == utf8.RuneError { 381 | return dflt, fmt.Errorf(`%v:"%v" has a rune error`, k, tv) 382 | } else if size != len(tv) { 383 | return r, fmt.Errorf(`%v:"%v" is more than a single rune`, k, tv) 384 | } 385 | return r, nil 386 | } 387 | -------------------------------------------------------------------------------- /tag_test.go: -------------------------------------------------------------------------------- 1 | package kong_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | 9 | "github.com/alecthomas/kong" 10 | ) 11 | 12 | func TestDefaultValueForOptionalArg(t *testing.T) { 13 | var cli struct { 14 | Arg string `kong:"arg,optional,default='\"\\'👌\\'\"'"` 15 | } 16 | p := mustNew(t, &cli) 17 | _, err := p.Parse(nil) 18 | assert.NoError(t, err) 19 | assert.Equal(t, "\"'👌'\"", cli.Arg) 20 | } 21 | 22 | func TestNoValueInTag(t *testing.T) { 23 | var cli struct { 24 | Empty1 string `kong:"default"` 25 | Empty2 string `kong:"default="` 26 | } 27 | p := mustNew(t, &cli) 28 | _, err := p.Parse(nil) 29 | assert.NoError(t, err) 30 | assert.Equal(t, "", cli.Empty1) 31 | assert.Equal(t, "", cli.Empty2) 32 | } 33 | 34 | func TestCommaInQuotes(t *testing.T) { 35 | var cli struct { 36 | Numbers string `kong:"default='1,2'"` 37 | } 38 | p := mustNew(t, &cli) 39 | _, err := p.Parse(nil) 40 | assert.NoError(t, err) 41 | assert.Equal(t, "1,2", cli.Numbers) 42 | } 43 | 44 | func TestBadString(t *testing.T) { 45 | var cli struct { 46 | Numbers string `kong:"default='yay'n"` 47 | } 48 | _, err := kong.New(&cli) 49 | assert.Error(t, err) 50 | } 51 | 52 | func TestNoQuoteEnd(t *testing.T) { 53 | var cli struct { 54 | Numbers string `kong:"default='yay"` 55 | } 56 | _, err := kong.New(&cli) 57 | assert.Error(t, err) 58 | } 59 | 60 | func TestEscapedQuote(t *testing.T) { 61 | var cli struct { 62 | DoYouKnow string `kong:"default='i don\\'t know'"` 63 | } 64 | p := mustNew(t, &cli) 65 | _, err := p.Parse(nil) 66 | assert.NoError(t, err) 67 | assert.Equal(t, "i don't know", cli.DoYouKnow) 68 | } 69 | 70 | func TestEscapingInQuotedTags(t *testing.T) { 71 | var cli struct { 72 | Regex1 string `kong:"default='\\d+\n'"` 73 | Regex2 string `default:"\\d+\n"` 74 | } 75 | p := mustNew(t, &cli) 76 | _, err := p.Parse(nil) 77 | assert.NoError(t, err) 78 | assert.Equal(t, "\\d+\n", cli.Regex1) 79 | assert.Equal(t, "\\d+\n", cli.Regex2) 80 | } 81 | 82 | func TestBareTags(t *testing.T) { 83 | var cli struct { 84 | Cmd struct { 85 | Arg string `arg` 86 | Flag string `required default:"👌"` 87 | } `cmd` 88 | } 89 | 90 | p := mustNew(t, &cli) 91 | _, err := p.Parse([]string{"cmd", "arg", "--flag=hi"}) 92 | assert.NoError(t, err) 93 | assert.Equal(t, "hi", cli.Cmd.Flag) 94 | assert.Equal(t, "arg", cli.Cmd.Arg) 95 | } 96 | 97 | func TestBareTagsWithJsonTag(t *testing.T) { 98 | var cli struct { 99 | Cmd struct { 100 | Arg string `json:"-" optional arg` 101 | Flag string `json:"best_flag" default:"\"'👌'\""` 102 | } `cmd json:"CMD"` 103 | } 104 | 105 | p := mustNew(t, &cli) 106 | _, err := p.Parse([]string{"cmd"}) 107 | assert.NoError(t, err) 108 | assert.Equal(t, "\"'👌'\"", cli.Cmd.Flag) 109 | assert.Equal(t, "", cli.Cmd.Arg) 110 | } 111 | 112 | func TestManySeps(t *testing.T) { 113 | var cli struct { 114 | Arg string `arg optional default:"hi"` 115 | } 116 | 117 | p := mustNew(t, &cli) 118 | _, err := p.Parse([]string{}) 119 | assert.NoError(t, err) 120 | assert.Equal(t, "hi", cli.Arg) 121 | } 122 | 123 | func TestTagSetOnEmbeddedStruct(t *testing.T) { 124 | type Embedded struct { 125 | Key string `help:"A key from ${where}."` 126 | } 127 | var cli struct { 128 | Embedded `set:"where=somewhere"` 129 | } 130 | buf := &strings.Builder{} 131 | p := mustNew(t, &cli, kong.Writers(buf, buf), kong.Exit(func(int) {})) 132 | _, err := p.Parse([]string{"--help"}) 133 | assert.NoError(t, err) 134 | assert.Contains(t, buf.String(), `A key from somewhere.`) 135 | } 136 | 137 | func TestTagSetOnCommand(t *testing.T) { 138 | type Command struct { 139 | Key string `help:"A key from ${where}."` 140 | } 141 | var cli struct { 142 | Command Command `set:"where=somewhere" cmd:""` 143 | } 144 | buf := &strings.Builder{} 145 | p := mustNew(t, &cli, kong.Writers(buf, buf), kong.Exit(func(int) {})) 146 | _, err := p.Parse([]string{"command", "--help"}) 147 | assert.NoError(t, err) 148 | assert.Contains(t, buf.String(), `A key from somewhere.`) 149 | } 150 | 151 | func TestTagSetOnFlag(t *testing.T) { 152 | var cli struct { 153 | Flag string `set:"where=somewhere" help:"A key from ${where}."` 154 | } 155 | buf := &strings.Builder{} 156 | p := mustNew(t, &cli, kong.Writers(buf, buf), kong.Exit(func(int) {})) 157 | _, err := p.Parse([]string{"--help"}) 158 | assert.NoError(t, err) 159 | assert.Contains(t, buf.String(), `A key from somewhere.`) 160 | } 161 | 162 | func TestTagAliases(t *testing.T) { 163 | type Command struct { 164 | Arg string `arg help:"Some arg"` 165 | } 166 | var cli struct { 167 | Cmd Command `cmd aliases:"alias1, alias2"` 168 | } 169 | p := mustNew(t, &cli) 170 | _, err := p.Parse([]string{"alias1", "arg"}) 171 | assert.NoError(t, err) 172 | assert.Equal(t, "arg", cli.Cmd.Arg) 173 | _, err = p.Parse([]string{"alias2", "arg"}) 174 | assert.NoError(t, err) 175 | assert.Equal(t, "arg", cli.Cmd.Arg) 176 | } 177 | 178 | func TestTagAliasesConflict(t *testing.T) { 179 | type Command struct { 180 | Arg string `arg help:"Some arg"` 181 | } 182 | var cli struct { 183 | Cmd Command `cmd hidden aliases:"other-cmd"` 184 | OtherCmd Command `cmd` 185 | } 186 | p := mustNew(t, &cli) 187 | _, err := p.Parse([]string{"other-cmd", "arg"}) 188 | assert.NoError(t, err) 189 | assert.Equal(t, "arg", cli.OtherCmd.Arg) 190 | } 191 | 192 | func TestTagAliasesSub(t *testing.T) { 193 | type SubCommand struct { 194 | Arg string `arg help:"Some arg"` 195 | } 196 | type Command struct { 197 | SubCmd SubCommand `cmd aliases:"other-sub-cmd"` 198 | } 199 | var cli struct { 200 | Cmd Command `cmd hidden` 201 | } 202 | p := mustNew(t, &cli) 203 | _, err := p.Parse([]string{"cmd", "other-sub-cmd", "arg"}) 204 | assert.NoError(t, err) 205 | assert.Equal(t, "arg", cli.Cmd.SubCmd.Arg) 206 | } 207 | 208 | func TestInvalidRuneErrors(t *testing.T) { 209 | cli := struct { 210 | Flag bool `short:"invalid"` 211 | }{} 212 | _, err := kong.New(&cli) 213 | assert.EqualError(t, err, ".Flag: invalid short flag name \"invalid\": invalid rune") 214 | } 215 | -------------------------------------------------------------------------------- /testdata/file.txt: -------------------------------------------------------------------------------- 1 | Hello world. -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | ) 8 | 9 | // ConfigFlag uses the configured (via kong.Configuration(loader)) configuration loader to load configuration 10 | // from a file specified by a flag. 11 | // 12 | // Use this as a flag value to support loading of custom configuration via a flag. 13 | type ConfigFlag string 14 | 15 | // BeforeResolve adds a resolver. 16 | func (c ConfigFlag) BeforeResolve(kong *Kong, ctx *Context, trace *Path) error { 17 | if kong.loader == nil { 18 | return fmt.Errorf("kong must be configured with kong.Configuration(...)") 19 | } 20 | path := string(ctx.FlagValue(trace.Flag).(ConfigFlag)) //nolint 21 | resolver, err := kong.LoadConfig(path) 22 | if err != nil { 23 | return err 24 | } 25 | ctx.AddResolver(resolver) 26 | return nil 27 | } 28 | 29 | // VersionFlag is a flag type that can be used to display a version number, stored in the "version" variable. 30 | type VersionFlag bool 31 | 32 | // BeforeReset writes the version variable and terminates with a 0 exit status. 33 | func (v VersionFlag) BeforeReset(app *Kong, vars Vars) error { 34 | fmt.Fprintln(app.Stdout, vars["version"]) 35 | app.Exit(0) 36 | return nil 37 | } 38 | 39 | // ChangeDirFlag changes the current working directory to a path specified by a flag 40 | // early in the parsing process, changing how other flags resolve relative paths. 41 | // 42 | // Use this flag to provide a "git -C" like functionality. 43 | // 44 | // It is not compatible with custom named decoders, e.g., existingdir. 45 | type ChangeDirFlag string 46 | 47 | // Decode is used to create a side effect of changing the current working directory. 48 | func (c ChangeDirFlag) Decode(ctx *DecodeContext) error { 49 | var path string 50 | err := ctx.Scan.PopValueInto("string", &path) 51 | if err != nil { 52 | return err 53 | } 54 | path = ExpandPath(path) 55 | ctx.Value.Target.Set(reflect.ValueOf(ChangeDirFlag(path))) 56 | return os.Chdir(path) 57 | } 58 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/alecthomas/assert/v2" 11 | ) 12 | 13 | func TestConfigFlag(t *testing.T) { 14 | var cli struct { 15 | Config ConfigFlag 16 | Flag string 17 | } 18 | 19 | w, err := os.CreateTemp(t.TempDir(), "") 20 | assert.NoError(t, err) 21 | w.WriteString(`{"flag": "hello world"}`) //nolint: errcheck 22 | w.Close() 23 | 24 | p := Must(&cli, Configuration(JSON)) 25 | _, err = p.Parse([]string{"--config", w.Name()}) 26 | assert.NoError(t, err) 27 | assert.Equal(t, "hello world", cli.Flag) 28 | } 29 | 30 | func TestVersionFlag(t *testing.T) { 31 | var cli struct { 32 | Version VersionFlag 33 | } 34 | w := &strings.Builder{} 35 | p := Must(&cli, Vars{"version": "0.1.1"}) 36 | p.Stdout = w 37 | called := 1 38 | p.Exit = func(s int) { called = s } 39 | 40 | _, err := p.Parse([]string{"--version"}) 41 | assert.NoError(t, err) 42 | assert.Equal(t, "0.1.1", strings.TrimSpace(w.String())) 43 | assert.Equal(t, 0, called) 44 | } 45 | 46 | func TestChangeDirFlag(t *testing.T) { 47 | cwd, err := os.Getwd() 48 | assert.NoError(t, err) 49 | defer os.Chdir(cwd) //nolint: errcheck 50 | 51 | dir := t.TempDir() 52 | file := filepath.Join(dir, "out.txt") 53 | err = os.WriteFile(file, []byte("foobar"), 0o600) 54 | assert.NoError(t, err) 55 | 56 | var cli struct { 57 | ChangeDir ChangeDirFlag `short:"C"` 58 | Path string `arg:"" type:"existingfile"` 59 | } 60 | 61 | p := Must(&cli) 62 | _, err = p.Parse([]string{"-C", dir, "out.txt"}) 63 | assert.NoError(t, err) 64 | if runtime.GOOS != "windows" { 65 | file, err = filepath.EvalSymlinks(file) // Needed because OSX uses a symlinked tmp dir. 66 | assert.NoError(t, err) 67 | } 68 | assert.Equal(t, file, cli.Path) 69 | } 70 | -------------------------------------------------------------------------------- /visit.go: -------------------------------------------------------------------------------- 1 | package kong 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Next should be called by Visitor to proceed with the walk. 8 | // 9 | // The walk will terminate if "err" is non-nil. 10 | type Next func(err error) error 11 | 12 | // Visitor can be used to walk all nodes in the model. 13 | type Visitor func(node Visitable, next Next) error 14 | 15 | // Visit all nodes. 16 | func Visit(node Visitable, visitor Visitor) error { 17 | return visitor(node, func(err error) error { 18 | if err != nil { 19 | return err 20 | } 21 | switch node := node.(type) { 22 | case *Application: 23 | return visitNodeChildren(node.Node, visitor) 24 | case *Node: 25 | return visitNodeChildren(node, visitor) 26 | case *Value: 27 | case *Flag: 28 | return Visit(node.Value, visitor) 29 | default: 30 | panic(fmt.Sprintf("unsupported node type %T", node)) 31 | } 32 | return nil 33 | }) 34 | } 35 | 36 | func visitNodeChildren(node *Node, visitor Visitor) error { 37 | if node.Argument != nil { 38 | if err := Visit(node.Argument, visitor); err != nil { 39 | return err 40 | } 41 | } 42 | for _, flag := range node.Flags { 43 | if err := Visit(flag, visitor); err != nil { 44 | return err 45 | } 46 | } 47 | for _, pos := range node.Positional { 48 | if err := Visit(pos, visitor); err != nil { 49 | return err 50 | } 51 | } 52 | for _, child := range node.Children { 53 | if err := Visit(child, visitor); err != nil { 54 | return err 55 | } 56 | } 57 | return nil 58 | } 59 | --------------------------------------------------------------------------------