├── .travis.yml ├── COPYING ├── README.md ├── _examples ├── chat1 │ └── main.go ├── chat2 │ └── main.go ├── completion │ └── main.go ├── curl │ └── main.go ├── modular │ └── main.go └── ping │ └── main.go ├── actions.go ├── app.go ├── app_test.go ├── args.go ├── args_test.go ├── cmd.go ├── cmd └── genvalues │ └── main.go ├── cmd_test.go ├── completions.go ├── completions_test.go ├── doc.go ├── envar.go ├── examples_test.go ├── flags.go ├── flags_test.go ├── global.go ├── guesswidth.go ├── guesswidth_unix.go ├── model.go ├── parser.go ├── parser_test.go ├── parsers.go ├── parsers_test.go ├── templates.go ├── usage.go ├── usage_test.go ├── values.go ├── values.json ├── values_generated.go └── values_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | install: go get -t -v ./... 4 | go: 1.2 5 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kingpin - A Go (golang) command line and flag parser 2 | [![](https://godoc.org/github.com/alecthomas/kingpin?status.svg)](http://godoc.org/github.com/alecthomas/kingpin) [![Build Status](https://travis-ci.org/alecthomas/kingpin.svg?branch=master)](https://travis-ci.org/alecthomas/kingpin) [![Gitter chat](https://badges.gitter.im/alecthomas.png)](https://gitter.im/alecthomas/Lobby) 3 | 4 | 5 | 6 | 7 | 8 | - [Overview](#overview) 9 | - [Features](#features) 10 | - [User-visible changes between v1 and v2](#user-visible-changes-between-v1-and-v2) 11 | - [Flags can be used at any point after their definition.](#flags-can-be-used-at-any-point-after-their-definition) 12 | - [Short flags can be combined with their parameters](#short-flags-can-be-combined-with-their-parameters) 13 | - [API changes between v1 and v2](#api-changes-between-v1-and-v2) 14 | - [Versions](#versions) 15 | - [V2 is the current stable version](#v2-is-the-current-stable-version) 16 | - [V1 is the OLD stable version](#v1-is-the-old-stable-version) 17 | - [Change History](#change-history) 18 | - [Examples](#examples) 19 | - [Simple Example](#simple-example) 20 | - [Complex Example](#complex-example) 21 | - [Reference Documentation](#reference-documentation) 22 | - [Displaying errors and usage information](#displaying-errors-and-usage-information) 23 | - [Sub-commands](#sub-commands) 24 | - [Custom Parsers](#custom-parsers) 25 | - [Repeatable flags](#repeatable-flags) 26 | - [Boolean Values](#boolean-values) 27 | - [Default Values](#default-values) 28 | - [Place-holders in Help](#place-holders-in-help) 29 | - [Consuming all remaining arguments](#consuming-all-remaining-arguments) 30 | - [Bash/ZSH Shell Completion](#bashzsh-shell-completion) 31 | - [Supporting -h for help](#supporting--h-for-help) 32 | - [Custom help](#custom-help) 33 | 34 | 35 | 36 | ## Overview 37 | 38 | Kingpin is a [fluent-style](http://en.wikipedia.org/wiki/Fluent_interface), 39 | type-safe command-line parser. It supports flags, nested commands, and 40 | positional arguments. 41 | 42 | Install it with: 43 | 44 | $ go get gopkg.in/alecthomas/kingpin.v2 45 | 46 | It looks like this: 47 | 48 | ```go 49 | var ( 50 | verbose = kingpin.Flag("verbose", "Verbose mode.").Short('v').Bool() 51 | name = kingpin.Arg("name", "Name of user.").Required().String() 52 | ) 53 | 54 | func main() { 55 | kingpin.Parse() 56 | fmt.Printf("%v, %s\n", *verbose, *name) 57 | } 58 | ``` 59 | 60 | More [examples](https://github.com/alecthomas/kingpin/tree/master/_examples) are available. 61 | 62 | Second to parsing, providing the user with useful help is probably the most 63 | important thing a command-line parser does. Kingpin tries to provide detailed 64 | contextual help if `--help` is encountered at any point in the command line 65 | (excluding after `--`). 66 | 67 | ## Features 68 | 69 | - Help output that isn't as ugly as sin. 70 | - Fully [customisable help](#custom-help), via Go templates. 71 | - Parsed, type-safe flags (`kingpin.Flag("f", "help").Int()`) 72 | - Parsed, type-safe positional arguments (`kingpin.Arg("a", "help").Int()`). 73 | - Parsed, type-safe, arbitrarily deep commands (`kingpin.Command("c", "help")`). 74 | - Support for required flags and required positional arguments (`kingpin.Flag("f", "").Required().Int()`). 75 | - Support for arbitrarily nested default commands (`command.Default()`). 76 | - Callbacks per command, flag and argument (`kingpin.Command("c", "").Action(myAction)`). 77 | - POSIX-style short flag combining (`-a -b` -> `-ab`). 78 | - Short-flag+parameter combining (`-a parm` -> `-aparm`). 79 | - Read command-line from files (`@`). 80 | - Automatically generate man pages (`--help-man`). 81 | 82 | ## User-visible changes between v1 and v2 83 | 84 | ### Flags can be used at any point after their definition. 85 | 86 | Flags can be specified at any point after their definition, not just 87 | *immediately after their associated command*. From the chat example below, the 88 | following used to be required: 89 | 90 | ``` 91 | $ chat --server=chat.server.com:8080 post --image=~/Downloads/owls.jpg pics 92 | ``` 93 | 94 | But the following will now work: 95 | 96 | ``` 97 | $ chat post --server=chat.server.com:8080 --image=~/Downloads/owls.jpg pics 98 | ``` 99 | 100 | ### Short flags can be combined with their parameters 101 | 102 | Previously, if a short flag was used, any argument to that flag would have to 103 | be separated by a space. That is no longer the case. 104 | 105 | ## API changes between v1 and v2 106 | 107 | - `ParseWithFileExpansion()` is gone. The new parser directly supports expanding `@`. 108 | - Added `FatalUsage()` and `FatalUsageContext()` for displaying an error + usage and terminating. 109 | - `Dispatch()` renamed to `Action()`. 110 | - Added `ParseContext()` for parsing a command line into its intermediate context form without executing. 111 | - Added `Terminate()` function to override the termination function. 112 | - Added `UsageForContextWithTemplate()` for printing usage via a custom template. 113 | - Added `UsageTemplate()` for overriding the default template to use. Two templates are included: 114 | 1. `DefaultUsageTemplate` - default template. 115 | 2. `CompactUsageTemplate` - compact command template for larger applications. 116 | 117 | ## Versions 118 | 119 | Kingpin uses [gopkg.in](https://gopkg.in/alecthomas/kingpin) for versioning. 120 | 121 | The current stable version is [gopkg.in/alecthomas/kingpin.v2](https://gopkg.in/alecthomas/kingpin.v2). The previous version, [gopkg.in/alecthomas/kingpin.v1](https://gopkg.in/alecthomas/kingpin.v1), is deprecated and in maintenance mode. 122 | 123 | ### [V2](https://gopkg.in/alecthomas/kingpin.v2) is the current stable version 124 | 125 | Installation: 126 | 127 | ```sh 128 | $ go get gopkg.in/alecthomas/kingpin.v2 129 | ``` 130 | 131 | ### [V1](https://gopkg.in/alecthomas/kingpin.v1) is the OLD stable version 132 | 133 | Installation: 134 | 135 | ```sh 136 | $ go get gopkg.in/alecthomas/kingpin.v1 137 | ``` 138 | 139 | ## Change History 140 | 141 | - *2015-09-19* -- Stable v2.1.0 release. 142 | - Added `command.Default()` to specify a default command to use if no other 143 | command matches. This allows for convenient user shortcuts. 144 | - Exposed `HelpFlag` and `VersionFlag` for further customisation. 145 | - `Action()` and `PreAction()` added and both now support an arbitrary 146 | number of callbacks. 147 | - `kingpin.SeparateOptionalFlagsUsageTemplate`. 148 | - `--help-long` and `--help-man` (hidden by default) flags. 149 | - Flags are "interspersed" by default, but can be disabled with `app.Interspersed(false)`. 150 | - Added flags for all simple builtin types (int8, uint16, etc.) and slice variants. 151 | - Use `app.Writer(os.Writer)` to specify the default writer for all output functions. 152 | - Dropped `os.Writer` prefix from all printf-like functions. 153 | 154 | - *2015-05-22* -- Stable v2.0.0 release. 155 | - Initial stable release of v2.0.0. 156 | - Fully supports interspersed flags, commands and arguments. 157 | - Flags can be present at any point after their logical definition. 158 | - Application.Parse() terminates if commands are present and a command is not parsed. 159 | - Dispatch() -> Action(). 160 | - Actions are dispatched after all values are populated. 161 | - Override termination function (defaults to os.Exit). 162 | - Override output stream (defaults to os.Stderr). 163 | - Templatised usage help, with default and compact templates. 164 | - Make error/usage functions more consistent. 165 | - Support argument expansion from files by default (with @). 166 | - Fully public data model is available via .Model(). 167 | - Parser has been completely refactored. 168 | - Parsing and execution has been split into distinct stages. 169 | - Use `go generate` to generate repeated flags. 170 | - Support combined short-flag+argument: -fARG. 171 | 172 | - *2015-01-23* -- Stable v1.3.4 release. 173 | - Support "--" for separating flags from positional arguments. 174 | - Support loading flags from files (ParseWithFileExpansion()). Use @FILE as an argument. 175 | - Add post-app and post-cmd validation hooks. This allows arbitrary validation to be added. 176 | - A bunch of improvements to help usage and formatting. 177 | - Support arbitrarily nested sub-commands. 178 | 179 | - *2014-07-08* -- Stable v1.2.0 release. 180 | - Pass any value through to `Strings()` when final argument. 181 | Allows for values that look like flags to be processed. 182 | - Allow `--help` to be used with commands. 183 | - Support `Hidden()` flags. 184 | - Parser for [units.Base2Bytes](https://github.com/alecthomas/units) 185 | type. Allows for flags like `--ram=512MB` or `--ram=1GB`. 186 | - Add an `Enum()` value, allowing only one of a set of values 187 | to be selected. eg. `Flag(...).Enum("debug", "info", "warning")`. 188 | 189 | - *2014-06-27* -- Stable v1.1.0 release. 190 | - Bug fixes. 191 | - Always return an error (rather than panicing) when misconfigured. 192 | - `OpenFile(flag, perm)` value type added, for finer control over opening files. 193 | - Significantly improved usage formatting. 194 | 195 | - *2014-06-19* -- Stable v1.0.0 release. 196 | - Support [cumulative positional](#consuming-all-remaining-arguments) arguments. 197 | - Return error rather than panic when there are fatal errors not caught by 198 | the type system. eg. when a default value is invalid. 199 | - Use gokpg.in. 200 | 201 | - *2014-06-10* -- Place-holder streamlining. 202 | - Renamed `MetaVar` to `PlaceHolder`. 203 | - Removed `MetaVarFromDefault`. Kingpin now uses [heuristics](#place-holders-in-help) 204 | to determine what to display. 205 | 206 | ## Examples 207 | 208 | ### Simple Example 209 | 210 | Kingpin can be used for simple flag+arg applications like so: 211 | 212 | ``` 213 | $ ping --help 214 | usage: ping [] [] 215 | 216 | Flags: 217 | --debug Enable debug mode. 218 | --help Show help. 219 | -t, --timeout=5s Timeout waiting for ping. 220 | 221 | Args: 222 | IP address to ping. 223 | [] Number of packets to send 224 | $ ping 1.2.3.4 5 225 | Would ping: 1.2.3.4 with timeout 5s and count 0 226 | ``` 227 | 228 | From the following source: 229 | 230 | ```go 231 | package main 232 | 233 | import ( 234 | "fmt" 235 | 236 | "gopkg.in/alecthomas/kingpin.v2" 237 | ) 238 | 239 | var ( 240 | debug = kingpin.Flag("debug", "Enable debug mode.").Bool() 241 | timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration() 242 | ip = kingpin.Arg("ip", "IP address to ping.").Required().IP() 243 | count = kingpin.Arg("count", "Number of packets to send").Int() 244 | ) 245 | 246 | func main() { 247 | kingpin.Version("0.0.1") 248 | kingpin.Parse() 249 | fmt.Printf("Would ping: %s with timeout %s and count %d\n", *ip, *timeout, *count) 250 | } 251 | ``` 252 | 253 | ### Complex Example 254 | 255 | Kingpin can also produce complex command-line applications with global flags, 256 | subcommands, and per-subcommand flags, like this: 257 | 258 | ``` 259 | $ chat --help 260 | usage: chat [] [] [ ...] 261 | 262 | A command-line chat application. 263 | 264 | Flags: 265 | --help Show help. 266 | --debug Enable debug mode. 267 | --server=127.0.0.1 Server address. 268 | 269 | Commands: 270 | help [] 271 | Show help for a command. 272 | 273 | register 274 | Register a new user. 275 | 276 | post [] [] 277 | Post a message to a channel. 278 | 279 | $ chat help post 280 | usage: chat [] post [] [] 281 | 282 | Post a message to a channel. 283 | 284 | Flags: 285 | --image=IMAGE Image to post. 286 | 287 | Args: 288 | Channel to post to. 289 | [] Text to post. 290 | 291 | $ chat post --image=~/Downloads/owls.jpg pics 292 | ... 293 | ``` 294 | 295 | From this code: 296 | 297 | ```go 298 | package main 299 | 300 | import ( 301 | "os" 302 | "strings" 303 | "gopkg.in/alecthomas/kingpin.v2" 304 | ) 305 | 306 | var ( 307 | app = kingpin.New("chat", "A command-line chat application.") 308 | debug = app.Flag("debug", "Enable debug mode.").Bool() 309 | serverIP = app.Flag("server", "Server address.").Default("127.0.0.1").IP() 310 | 311 | register = app.Command("register", "Register a new user.") 312 | registerNick = register.Arg("nick", "Nickname for user.").Required().String() 313 | registerName = register.Arg("name", "Name of user.").Required().String() 314 | 315 | post = app.Command("post", "Post a message to a channel.") 316 | postImage = post.Flag("image", "Image to post.").File() 317 | postChannel = post.Arg("channel", "Channel to post to.").Required().String() 318 | postText = post.Arg("text", "Text to post.").Strings() 319 | ) 320 | 321 | func main() { 322 | switch kingpin.MustParse(app.Parse(os.Args[1:])) { 323 | // Register user 324 | case register.FullCommand(): 325 | println(*registerNick) 326 | 327 | // Post message 328 | case post.FullCommand(): 329 | if *postImage != nil { 330 | } 331 | text := strings.Join(*postText, " ") 332 | println("Post:", text) 333 | } 334 | } 335 | ``` 336 | 337 | ## Reference Documentation 338 | 339 | ### Displaying errors and usage information 340 | 341 | Kingpin exports a set of functions to provide consistent errors and usage 342 | information to the user. 343 | 344 | Error messages look something like this: 345 | 346 | : error: 347 | 348 | The functions on `Application` are: 349 | 350 | Function | Purpose 351 | ---------|-------------- 352 | `Errorf(format, args)` | Display a printf formatted error to the user. 353 | `Fatalf(format, args)` | As with Errorf, but also call the termination handler. 354 | `FatalUsage(format, args)` | As with Fatalf, but also print contextual usage information. 355 | `FatalUsageContext(context, format, args)` | As with Fatalf, but also print contextual usage information from a `ParseContext`. 356 | `FatalIfError(err, format, args)` | Conditionally print an error prefixed with format+args, then call the termination handler 357 | 358 | There are equivalent global functions in the kingpin namespace for the default 359 | `kingpin.CommandLine` instance. 360 | 361 | ### Sub-commands 362 | 363 | Kingpin supports nested sub-commands, with separate flag and positional 364 | arguments per sub-command. Note that positional arguments may only occur after 365 | sub-commands. 366 | 367 | For example: 368 | 369 | ```go 370 | var ( 371 | deleteCommand = kingpin.Command("delete", "Delete an object.") 372 | deleteUserCommand = deleteCommand.Command("user", "Delete a user.") 373 | deleteUserUIDFlag = deleteUserCommand.Flag("uid", "Delete user by UID rather than username.") 374 | deleteUserUsername = deleteUserCommand.Arg("username", "Username to delete.") 375 | deletePostCommand = deleteCommand.Command("post", "Delete a post.") 376 | ) 377 | 378 | func main() { 379 | switch kingpin.Parse() { 380 | case "delete user": 381 | case "delete post": 382 | } 383 | } 384 | ``` 385 | 386 | ### Custom Parsers 387 | 388 | Kingpin supports both flag and positional argument parsers for converting to 389 | Go types. For example, some included parsers are `Int()`, `Float()`, 390 | `Duration()` and `ExistingFile()` (see [parsers.go](./parsers.go) for a complete list of included parsers). 391 | 392 | Parsers conform to Go's [`flag.Value`](http://godoc.org/flag#Value) 393 | interface, so any existing implementations will work. 394 | 395 | For example, a parser for accumulating HTTP header values might look like this: 396 | 397 | ```go 398 | type HTTPHeaderValue http.Header 399 | 400 | func (h *HTTPHeaderValue) Set(value string) error { 401 | parts := strings.SplitN(value, ":", 2) 402 | if len(parts) != 2 { 403 | return fmt.Errorf("expected HEADER:VALUE got '%s'", value) 404 | } 405 | (*http.Header)(h).Add(parts[0], parts[1]) 406 | return nil 407 | } 408 | 409 | func (h *HTTPHeaderValue) String() string { 410 | return "" 411 | } 412 | ``` 413 | 414 | As a convenience, I would recommend something like this: 415 | 416 | ```go 417 | func HTTPHeader(s Settings) (target *http.Header) { 418 | target = &http.Header{} 419 | s.SetValue((*HTTPHeaderValue)(target)) 420 | return 421 | } 422 | ``` 423 | 424 | You would use it like so: 425 | 426 | ```go 427 | headers = HTTPHeader(kingpin.Flag("header", "Add a HTTP header to the request.").Short('H')) 428 | ``` 429 | 430 | ### Repeatable flags 431 | 432 | Depending on the `Value` they hold, some flags may be repeated. The 433 | `IsCumulative() bool` function on `Value` tells if it's safe to call `Set()` 434 | multiple times or if an error should be raised if several values are passed. 435 | 436 | The built-in `Value`s returning slices and maps, as well as `Counter` are 437 | examples of `Value`s that make a flag repeatable. 438 | 439 | ### Boolean values 440 | 441 | Boolean values are uniquely managed by Kingpin. Each boolean flag will have a negative complement: 442 | `--` and `--no-`. 443 | 444 | ### Default Values 445 | 446 | The default value is the zero value for a type. This can be overridden with 447 | the `Default(value...)` function on flags and arguments. This function accepts 448 | one or several strings, which are parsed by the value itself, so they *must* 449 | be compliant with the format expected. 450 | 451 | ### Place-holders in Help 452 | 453 | The place-holder value for a flag is the value used in the help to describe 454 | the value of a non-boolean flag. 455 | 456 | The value provided to PlaceHolder() is used if provided, then the value 457 | provided by Default() if provided, then finally the capitalised flag name is 458 | used. 459 | 460 | Here are some examples of flags with various permutations: 461 | 462 | --name=NAME // Flag(...).String() 463 | --name="Harry" // Flag(...).Default("Harry").String() 464 | --name=FULL-NAME // flag(...).PlaceHolder("FULL-NAME").Default("Harry").String() 465 | 466 | ### Consuming all remaining arguments 467 | 468 | A common command-line idiom is to use all remaining arguments for some 469 | purpose. eg. The following command accepts an arbitrary number of 470 | IP addresses as positional arguments: 471 | 472 | ./cmd ping 10.1.1.1 192.168.1.1 473 | 474 | Such arguments are similar to [repeatable flags](#repeatable-flags), but for 475 | arguments. Therefore they use the same `IsCumulative() bool` function on the 476 | underlying `Value`, so the built-in `Value`s for which the `Set()` function 477 | can be called several times will consume multiple arguments. 478 | 479 | To implement the above example with a custom `Value`, we might do something 480 | like this: 481 | 482 | ```go 483 | type ipList []net.IP 484 | 485 | func (i *ipList) Set(value string) error { 486 | if ip := net.ParseIP(value); ip == nil { 487 | return fmt.Errorf("'%s' is not an IP address", value) 488 | } else { 489 | *i = append(*i, ip) 490 | return nil 491 | } 492 | } 493 | 494 | func (i *ipList) String() string { 495 | return "" 496 | } 497 | 498 | func (i *ipList) IsCumulative() bool { 499 | return true 500 | } 501 | 502 | func IPList(s Settings) (target *[]net.IP) { 503 | target = new([]net.IP) 504 | s.SetValue((*ipList)(target)) 505 | return 506 | } 507 | ``` 508 | 509 | And use it like so: 510 | 511 | ```go 512 | ips := IPList(kingpin.Arg("ips", "IP addresses to ping.")) 513 | ``` 514 | 515 | ### Bash/ZSH Shell Completion 516 | 517 | By default, all flags and commands/subcommands generate completions 518 | internally. 519 | 520 | Out of the box, CLI tools using kingpin should be able to take advantage 521 | of completion hinting for flags and commands. By specifying 522 | `--completion-bash` as the first argument, your CLI tool will show 523 | possible subcommands. By ending your argv with `--`, hints for flags 524 | will be shown. 525 | 526 | To allow your end users to take advantage you must package a 527 | `/etc/bash_completion.d` script with your distribution (or the equivalent 528 | for your target platform/shell). An alternative is to instruct your end 529 | user to source a script from their `bash_profile` (or equivalent). 530 | 531 | Fortunately Kingpin makes it easy to generate or source a script for use 532 | with end users shells. `./yourtool --completion-script-bash` and 533 | `./yourtool --completion-script-zsh` will generate these scripts for you. 534 | 535 | **Installation by Package** 536 | 537 | For the best user experience, you should bundle your pre-created 538 | completion script with your CLI tool and install it inside 539 | `/etc/bash_completion.d` (or equivalent). A good suggestion is to add 540 | this as an automated step to your build pipeline, in the implementation 541 | is improved for bug fixed. 542 | 543 | **Installation by `bash_profile`** 544 | 545 | Alternatively, instruct your users to add an additional statement to 546 | their `bash_profile` (or equivalent): 547 | 548 | ``` 549 | eval "$(your-cli-tool --completion-script-bash)" 550 | ``` 551 | 552 | Or for ZSH 553 | 554 | ``` 555 | eval "$(your-cli-tool --completion-script-zsh)" 556 | ``` 557 | 558 | #### Additional API 559 | To provide more flexibility, a completion option API has been 560 | exposed for flags to allow user defined completion options, to extend 561 | completions further than just EnumVar/Enum. 562 | 563 | 564 | **Provide Static Options** 565 | 566 | When using an `Enum` or `EnumVar`, users are limited to only the options 567 | given. Maybe we wish to hint possible options to the user, but also 568 | allow them to provide their own custom option. `HintOptions` gives 569 | this functionality to flags. 570 | 571 | ``` 572 | app := kingpin.New("completion", "My application with bash completion.") 573 | app.Flag("port", "Provide a port to connect to"). 574 | Required(). 575 | HintOptions("80", "443", "8080"). 576 | IntVar(&c.port) 577 | ``` 578 | 579 | **Provide Dynamic Options** 580 | Consider the case that you needed to read a local database or a file to 581 | provide suggestions. You can dynamically generate the options 582 | 583 | ``` 584 | func listHosts(args []string) []string { 585 | // Provide a dynamic list of hosts from a hosts file or otherwise 586 | // for bash completion. In this example we simply return static slice. 587 | 588 | // You could use this functionality to reach into a hosts file to provide 589 | // completion for a list of known hosts. 590 | return []string{"sshhost.example", "webhost.example", "ftphost.example"} 591 | } 592 | 593 | app := kingpin.New("completion", "My application with bash completion.") 594 | app.Flag("flag-1", "").HintAction(listHosts).String() 595 | ``` 596 | 597 | **EnumVar/Enum** 598 | When using `Enum` or `EnumVar`, any provided options will be automatically 599 | used for bash autocompletion. However, if you wish to provide a subset or 600 | different options, you can use `HintOptions` or `HintAction` which will override 601 | the default completion options for `Enum`/`EnumVar`. 602 | 603 | 604 | **Examples** 605 | You can see an in depth example of the completion API within 606 | `examples/completion/main.go` 607 | 608 | 609 | ### Supporting -h for help 610 | 611 | `kingpin.CommandLine.HelpFlag.Short('h')` 612 | 613 | ### Custom help 614 | 615 | Kingpin v2 supports templatised help using the text/template library (actually, [a fork](https://github.com/alecthomas/template)). 616 | 617 | You can specify the template to use with the [Application.UsageTemplate()](http://godoc.org/gopkg.in/alecthomas/kingpin.v2#Application.UsageTemplate) function. 618 | 619 | There are four included templates: `kingpin.DefaultUsageTemplate` is the default, 620 | `kingpin.CompactUsageTemplate` provides a more compact representation for more complex command-line structures, 621 | `kingpin.SeparateOptionalFlagsUsageTemplate` looks like the default template, but splits required 622 | and optional command flags into separate lists, and `kingpin.ManPageTemplate` is used to generate man pages. 623 | 624 | See the above templates for examples of usage, and the the function [UsageForContextWithTemplate()](https://github.com/alecthomas/kingpin/blob/master/usage.go#L198) method for details on the context. 625 | 626 | #### Default help template 627 | 628 | ``` 629 | $ go run ./examples/curl/curl.go --help 630 | usage: curl [] [ ...] 631 | 632 | An example implementation of curl. 633 | 634 | Flags: 635 | --help Show help. 636 | -t, --timeout=5s Set connection timeout. 637 | -H, --headers=HEADER=VALUE 638 | Add HTTP headers to the request. 639 | 640 | Commands: 641 | help [...] 642 | Show help. 643 | 644 | get url 645 | Retrieve a URL. 646 | 647 | get file 648 | Retrieve a file. 649 | 650 | post [] 651 | POST a resource. 652 | ``` 653 | 654 | #### Compact help template 655 | 656 | ``` 657 | $ go run ./examples/curl/curl.go --help 658 | usage: curl [] [ ...] 659 | 660 | An example implementation of curl. 661 | 662 | Flags: 663 | --help Show help. 664 | -t, --timeout=5s Set connection timeout. 665 | -H, --headers=HEADER=VALUE 666 | Add HTTP headers to the request. 667 | 668 | Commands: 669 | help [...] 670 | get [] 671 | url 672 | file 673 | post [] 674 | ``` 675 | -------------------------------------------------------------------------------- /_examples/chat1/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/alecthomas/kingpin.v2" 7 | ) 8 | 9 | var ( 10 | debug = kingpin.Flag("debug", "Enable debug mode.").Bool() 11 | timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration() 12 | ip = kingpin.Arg("ip", "IP address to ping.").Required().IP() 13 | count = kingpin.Arg("count", "Number of packets to send").Int() 14 | ) 15 | 16 | func main() { 17 | kingpin.Version("0.0.1") 18 | kingpin.Parse() 19 | fmt.Printf("Would ping: %s with timeout %s and count %d", *ip, *timeout, *count) 20 | } 21 | -------------------------------------------------------------------------------- /_examples/chat2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "gopkg.in/alecthomas/kingpin.v2" 8 | ) 9 | 10 | var ( 11 | app = kingpin.New("chat", "A command-line chat application.") 12 | debug = app.Flag("debug", "Enable debug mode.").Bool() 13 | serverIP = app.Flag("server", "Server address.").Default("127.0.0.1").IP() 14 | 15 | register = app.Command("register", "Register a new user.") 16 | registerNick = register.Arg("nick", "Nickname for user.").Required().String() 17 | registerName = register.Arg("name", "Name of user.").Required().String() 18 | 19 | post = app.Command("post", "Post a message to a channel.") 20 | postImage = post.Flag("image", "Image to post.").File() 21 | postChannel = post.Arg("channel", "Channel to post to.").Required().String() 22 | postText = post.Arg("text", "Text to post.").Strings() 23 | ) 24 | 25 | func main() { 26 | switch kingpin.MustParse(app.Parse(os.Args[1:])) { 27 | // Register user 28 | case register.FullCommand(): 29 | println(*registerNick) 30 | 31 | // Post message 32 | case post.FullCommand(): 33 | if *postImage != nil { 34 | } 35 | text := strings.Join(*postText, " ") 36 | println("Post:", text) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /_examples/completion/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/alecthomas/kingpin" 8 | ) 9 | 10 | func listHosts() []string { 11 | // Provide a dynamic list of hosts from a hosts file or otherwise 12 | // for bash completion. In this example we simply return static slice. 13 | 14 | // You could use this functionality to reach into a hosts file to provide 15 | // completion for a list of known hosts. 16 | return []string{"sshhost.example", "webhost.example", "ftphost.example"} 17 | } 18 | 19 | type NetcatCommand struct { 20 | hostName string 21 | port int 22 | format string 23 | } 24 | 25 | func (n *NetcatCommand) run(c *kingpin.ParseContext) error { 26 | fmt.Printf("Would have run netcat to hostname %v, port %d, and output format %v\n", n.hostName, n.port, n.format) 27 | return nil 28 | } 29 | 30 | func configureNetcatCommand(app *kingpin.Application) { 31 | c := &NetcatCommand{} 32 | nc := app.Command("nc", "Connect to a Host").Action(c.run) 33 | nc.Flag("nop-flag", "Example of a flag with no options").Bool() 34 | 35 | // You can provide hint options using a function to generate them 36 | nc.Flag("host", "Provide a hostname to nc"). 37 | Required(). 38 | HintAction(listHosts). 39 | StringVar(&c.hostName) 40 | 41 | // You can provide hint options statically 42 | nc.Flag("port", "Provide a port to connect to"). 43 | Required(). 44 | HintOptions("80", "443", "8080"). 45 | IntVar(&c.port) 46 | 47 | // Enum/EnumVar options will be turned into completion options automatically 48 | nc.Flag("format", "Define the output format"). 49 | Default("raw"). 50 | EnumVar(&c.format, "raw", "json") 51 | 52 | // You can combine HintOptions with HintAction too 53 | nc.Flag("host-with-multi", "Define a hostname"). 54 | HintAction(listHosts). 55 | HintOptions("myhost.com"). 56 | String() 57 | 58 | // And combine with themselves 59 | nc.Flag("host-with-multi-options", "Define a hostname"). 60 | HintOptions("myhost.com"). 61 | HintOptions("myhost2.com"). 62 | String() 63 | 64 | // If you specify HintOptions/HintActions for Enum/EnumVar, the options 65 | // provided for Enum/EnumVar will be overridden. 66 | nc.Flag("format-with-override-1", "Define a format"). 67 | HintAction(listHosts). 68 | Enum("option1", "option2") 69 | 70 | nc.Flag("format-with-override-2", "Define a format"). 71 | HintOptions("myhost.com", "myhost2.com"). 72 | Enum("option1", "option2") 73 | } 74 | 75 | func addSubCommand(app *kingpin.Application, name string, description string) { 76 | c := app.Command(name, description).Action(func(c *kingpin.ParseContext) error { 77 | fmt.Printf("Would have run command %s.\n", name) 78 | return nil 79 | }) 80 | c.Flag("nop-flag", "Example of a flag with no options").Bool() 81 | } 82 | 83 | func main() { 84 | app := kingpin.New("completion", "My application with bash completion.") 85 | app.Flag("flag-1", "").String() 86 | app.Flag("flag-2", "").HintOptions("opt1", "opt2").String() 87 | 88 | configureNetcatCommand(app) 89 | 90 | // Add some additional top level commands 91 | addSubCommand(app, "ls", "Additional top level command to show command completion") 92 | addSubCommand(app, "ping", "Additional top level command to show command completion") 93 | addSubCommand(app, "nmap", "Additional top level command to show command completion") 94 | 95 | kingpin.MustParse(app.Parse(os.Args[1:])) 96 | } 97 | -------------------------------------------------------------------------------- /_examples/curl/main.go: -------------------------------------------------------------------------------- 1 | // A curl-like HTTP command-line client. 2 | package main 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "strings" 11 | 12 | "gopkg.in/alecthomas/kingpin.v2" 13 | ) 14 | 15 | var ( 16 | timeout = kingpin.Flag("timeout", "Set connection timeout.").Short('t').Default("5s").Duration() 17 | headers = HTTPHeader(kingpin.Flag("headers", "Add HTTP headers to the request.").Short('H').PlaceHolder("HEADER=VALUE")) 18 | 19 | get = kingpin.Command("get", "GET a resource.").Default() 20 | getFlag = get.Flag("test", "Test flag").Bool() 21 | getURL = get.Command("url", "Retrieve a URL.").Default() 22 | getURLURL = getURL.Arg("url", "URL to GET.").Required().URL() 23 | getFile = get.Command("file", "Retrieve a file.") 24 | getFileFile = getFile.Arg("file", "File to retrieve.").Required().ExistingFile() 25 | 26 | post = kingpin.Command("post", "POST a resource.") 27 | postData = post.Flag("data", "Key-value data to POST").Short('d').PlaceHolder("KEY:VALUE").StringMap() 28 | postBinaryFile = post.Flag("data-binary", "File with binary data to POST.").File() 29 | postURL = post.Arg("url", "URL to POST to.").Required().URL() 30 | ) 31 | 32 | type HTTPHeaderValue http.Header 33 | 34 | func (h HTTPHeaderValue) Set(value string) error { 35 | parts := strings.SplitN(value, "=", 2) 36 | if len(parts) != 2 { 37 | return fmt.Errorf("expected HEADER=VALUE got '%s'", value) 38 | } 39 | (http.Header)(h).Add(parts[0], parts[1]) 40 | return nil 41 | } 42 | 43 | func (h HTTPHeaderValue) String() string { 44 | return "" 45 | } 46 | 47 | func HTTPHeader(s kingpin.Settings) (target *http.Header) { 48 | target = &http.Header{} 49 | s.SetValue((*HTTPHeaderValue)(target)) 50 | return 51 | } 52 | 53 | func applyRequest(req *http.Request) error { 54 | req.Header = *headers 55 | resp, err := http.DefaultClient.Do(req) 56 | if err != nil { 57 | return err 58 | } 59 | defer resp.Body.Close() 60 | if resp.StatusCode < 200 || resp.StatusCode > 299 { 61 | return fmt.Errorf("HTTP request failed: %s", resp.Status) 62 | } 63 | _, err = io.Copy(os.Stdout, resp.Body) 64 | return err 65 | } 66 | 67 | func apply(method string, url string) error { 68 | req, err := http.NewRequest(method, url, nil) 69 | if err != nil { 70 | return err 71 | } 72 | return applyRequest(req) 73 | } 74 | 75 | func applyPOST() error { 76 | req, err := http.NewRequest("POST", (*postURL).String(), nil) 77 | if err != nil { 78 | return err 79 | } 80 | if len(*postData) > 0 { 81 | for key, value := range *postData { 82 | req.Form.Set(key, value) 83 | } 84 | } else if postBinaryFile != nil { 85 | if headers.Get("Content-Type") != "" { 86 | headers.Set("Content-Type", "application/octet-stream") 87 | } 88 | req.Body = *postBinaryFile 89 | } else { 90 | return errors.New("--data or --data-binary must be provided to POST") 91 | } 92 | return applyRequest(req) 93 | } 94 | 95 | func main() { 96 | kingpin.UsageTemplate(kingpin.CompactUsageTemplate).Version("1.0").Author("Alec Thomas") 97 | kingpin.CommandLine.Help = "An example implementation of curl." 98 | switch kingpin.Parse() { 99 | case "get url": 100 | kingpin.FatalIfError(apply("GET", (*getURLURL).String()), "GET failed") 101 | 102 | case "post": 103 | kingpin.FatalIfError(applyPOST(), "POST failed") 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /_examples/modular/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "gopkg.in/alecthomas/kingpin.v2" 8 | ) 9 | 10 | // Context for "ls" command 11 | type LsCommand struct { 12 | All bool 13 | } 14 | 15 | func (l *LsCommand) run(c *kingpin.ParseContext) error { 16 | fmt.Printf("all=%v\n", l.All) 17 | return nil 18 | } 19 | 20 | func configureLsCommand(app *kingpin.Application) { 21 | c := &LsCommand{} 22 | ls := app.Command("ls", "List files.").Action(c.run) 23 | ls.Flag("all", "List all files.").Short('a').BoolVar(&c.All) 24 | } 25 | 26 | func main() { 27 | app := kingpin.New("modular", "My modular application.") 28 | configureLsCommand(app) 29 | kingpin.MustParse(app.Parse(os.Args[1:])) 30 | } 31 | -------------------------------------------------------------------------------- /_examples/ping/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/alecthomas/kingpin.v2" 7 | ) 8 | 9 | var ( 10 | debug = kingpin.Flag("debug", "Enable debug mode.").Bool() 11 | timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").OverrideDefaultFromEnvar("PING_TIMEOUT").Required().Short('t').Duration() 12 | ip = kingpin.Arg("ip", "IP address to ping.").Required().IP() 13 | count = kingpin.Arg("count", "Number of packets to send").Int() 14 | ) 15 | 16 | func main() { 17 | kingpin.Version("0.0.1") 18 | kingpin.Parse() 19 | fmt.Printf("Would ping: %s with timeout %s and count %d", *ip, *timeout, *count) 20 | } 21 | -------------------------------------------------------------------------------- /actions.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | // Action callback executed at various stages after all values are populated. 4 | // The application, commands, arguments and flags all have corresponding 5 | // actions. 6 | type Action func(*ParseContext) error 7 | 8 | type actionMixin struct { 9 | actions []Action 10 | preActions []Action 11 | postActions []Action 12 | } 13 | 14 | type actionApplier interface { 15 | applyActions(*ParseContext) error 16 | applyPreActions(*ParseContext) error 17 | } 18 | 19 | func (a *actionMixin) addAction(action Action) { 20 | a.actions = append(a.actions, action) 21 | } 22 | 23 | func (a *actionMixin) addPreAction(action Action) { 24 | a.preActions = append(a.preActions, action) 25 | } 26 | 27 | func (a *actionMixin) addPostAction(action Action) { 28 | a.postActions = append(a.postActions, action) 29 | } 30 | 31 | func (a *actionMixin) applyActions(context *ParseContext) error { 32 | for _, action := range a.actions { 33 | if err := action(context); err != nil { 34 | return err 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func (a *actionMixin) applyPreActions(context *ParseContext) error { 41 | for _, preAction := range a.preActions { 42 | if err := preAction(context); err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | func (a *actionMixin) applyPostActions(context *ParseContext) error { 50 | for _, postActions := range a.postActions { 51 | if err := postActions(context); err != nil { 52 | return err 53 | } 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | ErrCommandNotSpecified = fmt.Errorf("command not specified") 13 | ) 14 | 15 | var ( 16 | envarTransformRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]+`) 17 | ) 18 | 19 | type ApplicationValidator func(*Application) error 20 | 21 | // An Application contains the definitions of flags, arguments and commands 22 | // for an application. 23 | type Application struct { 24 | cmdMixin 25 | initialized bool 26 | 27 | Name string 28 | Help string 29 | 30 | author string 31 | version string 32 | errorWriter io.Writer // Destination for errors. 33 | usageWriter io.Writer // Destination for usage 34 | usageTemplate string 35 | validator ApplicationValidator 36 | terminate func(status int) // See Terminate() 37 | noInterspersed bool // can flags be interspersed with args (or must they come first) 38 | defaultEnvars bool 39 | completion bool 40 | 41 | // Help flag. Exposed for user customisation. 42 | HelpFlag *FlagClause 43 | // Help command. Exposed for user customisation. May be nil. 44 | HelpCommand *Cmd 45 | // Version flag. Exposed for user customisation. May be nil. 46 | VersionFlag *FlagClause 47 | } 48 | 49 | // New creates a new Kingpin application instance. 50 | func New(name, help string) *Application { 51 | a := &Application{ 52 | Name: name, 53 | Help: help, 54 | errorWriter: os.Stderr, // Left for backwards compatibility purposes. 55 | usageWriter: os.Stderr, 56 | usageTemplate: DefaultUsageTemplate, 57 | terminate: os.Exit, 58 | } 59 | a.flagGroup = newFlagGroup() 60 | a.argGroup = newArgGroup() 61 | a.cmdGroup = newCmdGroup(a) 62 | a.HelpFlag = a.Flag("help", "Output usage information.").Short('h') 63 | a.HelpFlag.Bool() 64 | a.Flag("help-long", "Generate long help.").Hidden().PreAction(a.generateLongHelp).Bool() 65 | a.Flag("help-man", "Generate a man page.").Hidden().PreAction(a.generateManPage).Bool() 66 | a.Flag("completion-bash", "Output possible completions for the given args.").Hidden().BoolVar(&a.completion) 67 | a.Flag("completion-script-bash", "Generate completion script for bash.").Hidden().PreAction(a.generateBashCompletionScript).Bool() 68 | a.Flag("completion-script-zsh", "Generate completion script for ZSH.").Hidden().PreAction(a.generateZSHCompletionScript).Bool() 69 | 70 | return a 71 | } 72 | 73 | func (a *Application) generateLongHelp(c *ParseContext) error { 74 | a.Writer(os.Stdout) 75 | if err := a.UsageForContextWithTemplate(c, 2, LongHelpTemplate); err != nil { 76 | return err 77 | } 78 | a.terminate(0) 79 | return nil 80 | } 81 | 82 | func (a *Application) generateManPage(c *ParseContext) error { 83 | a.Writer(os.Stdout) 84 | if err := a.UsageForContextWithTemplate(c, 2, ManPageTemplate); err != nil { 85 | return err 86 | } 87 | a.terminate(0) 88 | return nil 89 | } 90 | 91 | func (a *Application) generateBashCompletionScript(c *ParseContext) error { 92 | a.Writer(os.Stdout) 93 | if err := a.UsageForContextWithTemplate(c, 2, BashCompletionTemplate); err != nil { 94 | return err 95 | } 96 | a.terminate(0) 97 | return nil 98 | } 99 | 100 | func (a *Application) generateZSHCompletionScript(c *ParseContext) error { 101 | a.Writer(os.Stdout) 102 | if err := a.UsageForContextWithTemplate(c, 2, ZshCompletionTemplate); err != nil { 103 | return err 104 | } 105 | a.terminate(0) 106 | return nil 107 | } 108 | 109 | // DefaultEnvars configures all flags (that do not already have an associated 110 | // envar) to use a default environment variable in the form "_". 111 | // 112 | // For example, if the application is named "foo" and a flag is named "bar- 113 | // waz" the environment variable: "FOO_BAR_WAZ". 114 | func (a *Application) DefaultEnvars() *Application { 115 | a.defaultEnvars = true 116 | return a 117 | } 118 | 119 | // Terminate specifies the termination handler. Defaults to os.Exit(status). 120 | // If nil is passed, a no-op function will be used. 121 | func (a *Application) Terminate(terminate func(int)) *Application { 122 | if terminate == nil { 123 | terminate = func(int) {} 124 | } 125 | a.terminate = terminate 126 | return a 127 | } 128 | 129 | // Writer specifies the writer to use for usage and errors. Defaults to os.Stderr. 130 | // DEPRECATED: See ErrorWriter and UsageWriter. 131 | func (a *Application) Writer(w io.Writer) *Application { 132 | a.errorWriter = w 133 | a.usageWriter = w 134 | return a 135 | } 136 | 137 | // ErrorWriter sets the io.Writer to use for errors. 138 | func (a *Application) ErrorWriter(w io.Writer) *Application { 139 | a.errorWriter = w 140 | return a 141 | } 142 | 143 | // UsageWriter sets the io.Writer to use for errors. 144 | func (a *Application) UsageWriter(w io.Writer) *Application { 145 | a.usageWriter = w 146 | return a 147 | } 148 | 149 | // UsageTemplate specifies the text template to use when displaying usage 150 | // information. The default is UsageTemplate. 151 | func (a *Application) UsageTemplate(template string) *Application { 152 | a.usageTemplate = template 153 | return a 154 | } 155 | 156 | // Validate sets a validation function to run when parsing. 157 | func (a *Application) Validate(validator ApplicationValidator) *Application { 158 | a.validator = validator 159 | return a 160 | } 161 | 162 | // ParseContext parses the given command line and returns the fully populated 163 | // ParseContext. 164 | func (a *Application) ParseContext(args []string) (*ParseContext, error) { 165 | return a.parseContext(false, args) 166 | } 167 | 168 | func (a *Application) parseContext(ignoreDefault bool, args []string) (*ParseContext, error) { 169 | if err := a.init(); err != nil { 170 | return nil, err 171 | } 172 | context := tokenize(args, ignoreDefault) 173 | err := parse(context, a) 174 | return context, err 175 | } 176 | 177 | // Parse parses command-line arguments. It returns the selected command and an 178 | // error. The selected command will be a space separated subcommand, if 179 | // subcommands have been configured. 180 | // 181 | // This will populate all flag and argument values, call all callbacks, and so 182 | // on. 183 | func (a *Application) Parse(args []string) (command string, err error) { 184 | 185 | context, parseErr := a.ParseContext(args) 186 | selected := []string{} 187 | var setValuesErr error 188 | 189 | if context == nil { 190 | // Since we do not throw error immediately, there could be a case 191 | // where a context returns nil. Protect against that. 192 | return "", parseErr 193 | } 194 | 195 | if err := a.setDefaults(context); err != nil { 196 | return "", err 197 | } 198 | 199 | selected, setValuesErr = a.setValues(context) 200 | 201 | if err := a.applyPreActions(context, !a.completion); err != nil { 202 | return "", err 203 | } 204 | 205 | if a.completion { 206 | a.generateBashCompletion(context) 207 | a.terminate(0) 208 | } else { 209 | if parseErr != nil { 210 | return "", parseErr 211 | } 212 | 213 | a.maybeHelp(context) 214 | if !context.EOL() { 215 | return "", fmt.Errorf("unexpected argument '%s'", context.Peek()) 216 | } 217 | 218 | if setValuesErr != nil { 219 | return "", setValuesErr 220 | } 221 | 222 | command, err = a.execute(context, selected) 223 | if err == ErrCommandNotSpecified { 224 | return "", nil 225 | } 226 | } 227 | 228 | if err := a.applyPostActions(context); err != nil { 229 | return "", err 230 | } 231 | 232 | return command, err 233 | } 234 | 235 | func (a *Application) writeUsage(context *ParseContext, err error) { 236 | if err != nil { 237 | a.Errorf("%s", err) 238 | } 239 | if err := a.UsageForContext(context); err != nil { 240 | panic(err) 241 | } 242 | if err != nil { 243 | a.terminate(1) 244 | } else { 245 | a.terminate(0) 246 | } 247 | } 248 | 249 | func (a *Application) maybeHelp(context *ParseContext) { 250 | for _, element := range context.Elements { 251 | if flag, ok := element.Clause.(*FlagClause); ok && flag == a.HelpFlag { 252 | // Re-parse the command-line ignoring defaults, so that help works correctly. 253 | context, _ = a.parseContext(true, context.rawArgs) 254 | a.writeUsage(context, nil) 255 | } 256 | } 257 | } 258 | 259 | // GetVersion returns the version. 260 | func (a *Application) GetVersion() string { 261 | return a.version 262 | } 263 | 264 | // Version adds a --version flag for displaying the application version. 265 | func (a *Application) Version(version string) *Application { 266 | a.version = version 267 | a.VersionFlag = a.Flag("version", "Show application version.").PreAction(func(*ParseContext) error { 268 | fmt.Fprintln(a.usageWriter, version) 269 | a.terminate(0) 270 | return nil 271 | }) 272 | a.VersionFlag.Bool() 273 | return a 274 | } 275 | 276 | // Author sets the author output by some help templates. 277 | func (a *Application) Author(author string) *Application { 278 | a.author = author 279 | return a 280 | } 281 | 282 | // Action callback to call when all values are populated and parsing is 283 | // complete, but before any command, flag or argument actions. 284 | // 285 | // All Action() callbacks are called in the order they are encountered on the 286 | // command line. 287 | func (a *Application) Action(action Action) *Application { 288 | a.addAction(action) 289 | return a 290 | } 291 | 292 | // PreAction called after parsing completes but before validation and execution. 293 | func (a *Application) PreAction(action Action) *Application { 294 | a.addPreAction(action) 295 | return a 296 | } 297 | 298 | // PostAction called after execution. 299 | func (a *Application) PostAction(action Action) *Application { 300 | a.addPostAction(action) 301 | return a 302 | } 303 | 304 | // Command adds a new top-level command. 305 | func (a *Application) Command(name, help string) *Cmd { 306 | return a.addCommand(name, help) 307 | } 308 | 309 | // Interspersed control if flags can be interspersed with positional arguments 310 | // 311 | // true (the default) means that they can, false means that all the flags must appear before the first positional arguments. 312 | func (a *Application) Interspersed(interspersed bool) *Application { 313 | a.noInterspersed = !interspersed 314 | return a 315 | } 316 | 317 | func (a *Application) defaultEnvarPrefix() string { 318 | if a.defaultEnvars { 319 | return a.Name 320 | } 321 | return "" 322 | } 323 | 324 | func (a *Application) init() error { 325 | if a.initialized { 326 | return nil 327 | } 328 | if a.cmdGroup.have() && a.argGroup.have() { 329 | return fmt.Errorf("can't mix top-level Arg()s with Command()s") 330 | } 331 | 332 | // If we have subcommands, add a help command at the top-level. 333 | if a.cmdGroup.have() { 334 | var command []string 335 | a.HelpCommand = a.Command("help", "Show help for a command.").PreAction(func(context *ParseContext) error { 336 | a.Usage(command) 337 | a.terminate(0) 338 | return nil 339 | }) 340 | a.HelpCommand.Arg("command", "Show help for a command.").StringsVar(&command) 341 | // Make help first command. 342 | l := len(a.commandOrder) 343 | a.commandOrder = append(a.commandOrder[l-1:l], a.commandOrder[:l-1]...) 344 | } 345 | 346 | if err := a.flagGroup.init(a.defaultEnvarPrefix()); err != nil { 347 | return err 348 | } 349 | if err := a.cmdGroup.init(); err != nil { 350 | return err 351 | } 352 | if err := a.argGroup.init(); err != nil { 353 | return err 354 | } 355 | for _, cmd := range a.commands { 356 | if err := cmd.init(); err != nil { 357 | return err 358 | } 359 | } 360 | flagGroups := []*flagGroup{a.flagGroup} 361 | for _, cmd := range a.commandOrder { 362 | if err := checkDuplicateFlags(cmd, flagGroups); err != nil { 363 | return err 364 | } 365 | } 366 | a.initialized = true 367 | return nil 368 | } 369 | 370 | // Recursively check commands for duplicate flags. 371 | func checkDuplicateFlags(current *Cmd, flagGroups []*flagGroup) error { 372 | // Check for duplicates. 373 | for _, flags := range flagGroups { 374 | for _, flag := range current.flagOrder { 375 | if flag.shorthand != 0 { 376 | if _, ok := flags.short[string(flag.shorthand)]; ok { 377 | return fmt.Errorf("duplicate short flag -%c", flag.shorthand) 378 | } 379 | } 380 | if _, ok := flags.long[flag.name]; ok { 381 | return fmt.Errorf("duplicate long flag --%s", flag.name) 382 | } 383 | } 384 | } 385 | flagGroups = append(flagGroups, current.flagGroup) 386 | // Check subcommands. 387 | for _, subcmd := range current.commandOrder { 388 | if err := checkDuplicateFlags(subcmd, flagGroups); err != nil { 389 | return err 390 | } 391 | } 392 | return nil 393 | } 394 | 395 | func (a *Application) execute(context *ParseContext, selected []string) (string, error) { 396 | var err error 397 | 398 | if err = a.validateRequired(context); err != nil { 399 | return "", err 400 | } 401 | 402 | if err = a.applyValidators(context); err != nil { 403 | return "", err 404 | } 405 | 406 | if err = a.applyActions(context); err != nil { 407 | return "", err 408 | } 409 | 410 | command := strings.Join(selected, " ") 411 | if command == "" && a.cmdGroup.have() { 412 | return "", ErrCommandNotSpecified 413 | } 414 | return command, err 415 | } 416 | 417 | func (a *Application) setDefaults(context *ParseContext) error { 418 | flagElements := map[string]*ParseElement{} 419 | for _, element := range context.Elements { 420 | if flag, ok := element.Clause.(*FlagClause); ok { 421 | flagElements[flag.name] = element 422 | } 423 | } 424 | 425 | argElements := map[string]*ParseElement{} 426 | for _, element := range context.Elements { 427 | if arg, ok := element.Clause.(*ArgClause); ok { 428 | argElements[arg.name] = element 429 | } 430 | } 431 | 432 | // Check required flags and set defaults. 433 | for _, flag := range context.flags.long { 434 | if flagElements[flag.name] == nil { 435 | if err := flag.setDefault(); err != nil { 436 | return err 437 | } 438 | } 439 | } 440 | 441 | for _, arg := range context.arguments.args { 442 | if argElements[arg.name] == nil { 443 | if err := arg.setDefault(); err != nil { 444 | return err 445 | } 446 | } 447 | } 448 | 449 | return nil 450 | } 451 | 452 | func (a *Application) validateRequired(context *ParseContext) error { 453 | flagElements := map[string]*ParseElement{} 454 | for _, element := range context.Elements { 455 | if flag, ok := element.Clause.(*FlagClause); ok { 456 | flagElements[flag.name] = element 457 | } 458 | } 459 | 460 | argElements := map[string]*ParseElement{} 461 | for _, element := range context.Elements { 462 | if arg, ok := element.Clause.(*ArgClause); ok { 463 | argElements[arg.name] = element 464 | } 465 | } 466 | 467 | // Check required flags and set defaults. 468 | for _, flag := range context.flags.long { 469 | if flagElements[flag.name] == nil { 470 | // Check required flags were provided. 471 | if flag.needsValue() { 472 | return fmt.Errorf("required flag --%s not provided", flag.name) 473 | } 474 | } 475 | } 476 | 477 | for _, arg := range context.arguments.args { 478 | if argElements[arg.name] == nil { 479 | if arg.needsValue() { 480 | return fmt.Errorf("required argument '%s' not provided", arg.name) 481 | } 482 | } 483 | } 484 | return nil 485 | } 486 | 487 | func (a *Application) setValues(context *ParseContext) (selected []string, err error) { 488 | // Set all arg and flag values. 489 | var ( 490 | lastCmd *Cmd 491 | flagSet = map[string]struct{}{} 492 | ) 493 | for _, element := range context.Elements { 494 | switch clause := element.Clause.(type) { 495 | case *FlagClause: 496 | if _, ok := flagSet[clause.name]; ok { 497 | if v, ok := clause.value.(repeatableFlag); !ok || !v.IsCumulative() { 498 | return nil, fmt.Errorf("flag '%s' cannot be repeated", clause.name) 499 | } 500 | } 501 | if err = clause.value.Set(*element.Value); err != nil { 502 | return 503 | } 504 | flagSet[clause.name] = struct{}{} 505 | 506 | case *ArgClause: 507 | if err = clause.value.Set(*element.Value); err != nil { 508 | return 509 | } 510 | 511 | case *Cmd: 512 | if clause.validator != nil { 513 | if err = clause.validator(clause); err != nil { 514 | return 515 | } 516 | } 517 | selected = append(selected, clause.name) 518 | lastCmd = clause 519 | } 520 | } 521 | 522 | if lastCmd != nil && len(lastCmd.commands) > 0 { 523 | return nil, fmt.Errorf("must select a subcommand of '%s'", lastCmd.FullCommand()) 524 | } 525 | 526 | return 527 | } 528 | 529 | func (a *Application) applyValidators(context *ParseContext) (err error) { 530 | // Call command validation functions. 531 | for _, element := range context.Elements { 532 | if cmd, ok := element.Clause.(*Cmd); ok && cmd.validator != nil { 533 | if err = cmd.validator(cmd); err != nil { 534 | return err 535 | } 536 | } 537 | } 538 | 539 | if a.validator != nil { 540 | err = a.validator(a) 541 | } 542 | return err 543 | } 544 | 545 | func (a *Application) applyPreActions(context *ParseContext, dispatch bool) error { 546 | if err := a.actionMixin.applyPreActions(context); err != nil { 547 | return err 548 | } 549 | // Dispatch to actions. 550 | if dispatch { 551 | for _, element := range context.Elements { 552 | if applier, ok := element.Clause.(actionApplier); ok { 553 | if err := applier.applyPreActions(context); err != nil { 554 | return err 555 | } 556 | } 557 | } 558 | } 559 | 560 | return nil 561 | } 562 | 563 | func (a *Application) applyActions(context *ParseContext) error { 564 | if err := a.actionMixin.applyActions(context); err != nil { 565 | return err 566 | } 567 | // Dispatch to actions. 568 | for _, element := range context.Elements { 569 | if applier, ok := element.Clause.(actionApplier); ok { 570 | if err := applier.applyActions(context); err != nil { 571 | return err 572 | } 573 | } 574 | } 575 | return nil 576 | } 577 | 578 | // Errorf prints an error message to w in the format ": error: ". 579 | func (a *Application) Errorf(format string, args ...interface{}) { 580 | fmt.Fprintf(a.errorWriter, a.Name+": error: "+format+"\n", args...) 581 | } 582 | 583 | // Fatalf writes a formatted error to w then terminates with exit status 1. 584 | func (a *Application) Fatalf(format string, args ...interface{}) { 585 | a.Errorf(format, args...) 586 | a.terminate(1) 587 | } 588 | 589 | // FatalUsage prints an error message followed by usage information, then 590 | // exits with a non-zero status. 591 | func (a *Application) FatalUsage(format string, args ...interface{}) { 592 | a.Errorf(format, args...) 593 | // Force usage to go to error output. 594 | a.usageWriter = a.errorWriter 595 | a.Usage([]string{}) 596 | a.terminate(1) 597 | } 598 | 599 | // FatalUsageContext writes a printf formatted error message to w, then usage 600 | // information for the given ParseContext, before exiting. 601 | func (a *Application) FatalUsageContext(context *ParseContext, format string, args ...interface{}) { 602 | a.Errorf(format, args...) 603 | if err := a.UsageForContext(context); err != nil { 604 | panic(err) 605 | } 606 | a.terminate(1) 607 | } 608 | 609 | // FatalIfError prints an error and exits if err is not nil. The error is printed 610 | // with the given formatted string, if any. 611 | func (a *Application) FatalIfError(err error, format string, args ...interface{}) { 612 | if err != nil { 613 | prefix := "" 614 | if format != "" { 615 | prefix = fmt.Sprintf(format, args...) + ": " 616 | } 617 | a.Errorf(prefix+"%s", err) 618 | a.terminate(1) 619 | } 620 | } 621 | 622 | func (a *Application) completionOptions(context *ParseContext) []string { 623 | args := context.rawArgs 624 | 625 | var ( 626 | currArg string 627 | prevArg string 628 | target cmdMixin 629 | ) 630 | 631 | numArgs := len(args) 632 | if numArgs > 1 { 633 | args = args[1:] 634 | currArg = args[len(args)-1] 635 | } 636 | if numArgs > 2 { 637 | prevArg = args[len(args)-2] 638 | } 639 | 640 | target = a.cmdMixin 641 | if context.SelectedCommand != nil { 642 | // A subcommand was in use. We will use it as the target 643 | target = context.SelectedCommand.cmdMixin 644 | } 645 | 646 | if (currArg != "" && strings.HasPrefix(currArg, "--")) || strings.HasPrefix(prevArg, "--") { 647 | // Perform completion for A flag. The last/current argument started with "-" 648 | var ( 649 | flagName string // The name of a flag if given (could be half complete) 650 | flagValue string // The value assigned to a flag (if given) (could be half complete) 651 | ) 652 | 653 | if strings.HasPrefix(prevArg, "--") && !strings.HasPrefix(currArg, "--") { 654 | // Matches: ./myApp --flag value 655 | // Wont Match: ./myApp --flag -- 656 | flagName = prevArg[2:] // Strip the "--" 657 | flagValue = currArg 658 | } else if strings.HasPrefix(currArg, "--") { 659 | // Matches: ./myApp --flag -- 660 | // Matches: ./myApp --flag somevalue -- 661 | // Matches: ./myApp -- 662 | flagName = currArg[2:] // Strip the "--" 663 | } 664 | 665 | options, flagMatched, valueMatched := target.FlagCompletion(flagName, flagValue) 666 | if valueMatched { 667 | // Value Matched. Show cmdCompletions 668 | return target.CmdCompletion(context) 669 | } 670 | 671 | // Add top level flags if we're not at the top level and no match was found. 672 | if context.SelectedCommand != nil && !flagMatched { 673 | topOptions, topFlagMatched, topValueMatched := a.FlagCompletion(flagName, flagValue) 674 | if topValueMatched { 675 | // Value Matched. Back to cmdCompletions 676 | return target.CmdCompletion(context) 677 | } 678 | 679 | if topFlagMatched { 680 | // Top level had a flag which matched the input. Return it's options. 681 | options = topOptions 682 | } else { 683 | // Add top level flags 684 | options = append(options, topOptions...) 685 | } 686 | } 687 | return options 688 | } 689 | 690 | // Perform completion for sub commands and arguments. 691 | return target.CmdCompletion(context) 692 | } 693 | 694 | func (a *Application) generateBashCompletion(context *ParseContext) { 695 | options := a.completionOptions(context) 696 | fmt.Printf("%s", strings.Join(options, "\n")) 697 | } 698 | 699 | func envarTransform(name string) string { 700 | return strings.ToUpper(envarTransformRegexp.ReplaceAllString(name, "_")) 701 | } 702 | -------------------------------------------------------------------------------- /app_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/alecthomas/assert" 7 | 8 | "sort" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func newTestApp() *Application { 15 | return New("test", "").Terminate(nil) 16 | } 17 | 18 | func TestCommander(t *testing.T) { 19 | c := newTestApp() 20 | ping := c.Command("ping", "Ping an IP address.") 21 | pingTTL := ping.Flag("ttl", "TTL for ICMP packets").Short('t').Default("5s").Duration() 22 | 23 | selected, err := c.Parse([]string{"ping"}) 24 | assert.NoError(t, err) 25 | assert.Equal(t, "ping", selected) 26 | assert.Equal(t, 5*time.Second, *pingTTL) 27 | 28 | selected, err = c.Parse([]string{"ping", "--ttl=10s"}) 29 | assert.NoError(t, err) 30 | assert.Equal(t, "ping", selected) 31 | assert.Equal(t, 10*time.Second, *pingTTL) 32 | } 33 | 34 | func TestRequiredFlags(t *testing.T) { 35 | c := newTestApp() 36 | c.Flag("a", "a").String() 37 | c.Flag("b", "b").Required().String() 38 | 39 | _, err := c.Parse([]string{"--a=foo"}) 40 | assert.Error(t, err) 41 | _, err = c.Parse([]string{"--b=foo"}) 42 | assert.NoError(t, err) 43 | } 44 | 45 | func TestRepeatableFlags(t *testing.T) { 46 | c := newTestApp() 47 | c.Flag("a", "a").String() 48 | c.Flag("b", "b").Strings() 49 | _, err := c.Parse([]string{"--a=foo", "--a=bar"}) 50 | assert.Error(t, err) 51 | _, err = c.Parse([]string{"--b=foo", "--b=bar"}) 52 | assert.NoError(t, err) 53 | } 54 | 55 | func TestInvalidDefaultFlagValueErrors(t *testing.T) { 56 | c := newTestApp() 57 | c.Flag("foo", "foo").Default("a").Int() 58 | _, err := c.Parse([]string{}) 59 | assert.Error(t, err) 60 | } 61 | 62 | func TestInvalidDefaultArgValueErrors(t *testing.T) { 63 | c := newTestApp() 64 | cmd := c.Command("cmd", "cmd") 65 | cmd.Arg("arg", "arg").Default("one").Int() 66 | _, err := c.Parse([]string{"cmd"}) 67 | assert.Error(t, err) 68 | } 69 | 70 | func TestArgsRequiredAfterNonRequiredErrors(t *testing.T) { 71 | c := newTestApp() 72 | cmd := c.Command("cmd", "") 73 | cmd.Arg("a", "a").String() 74 | cmd.Arg("b", "b").Required().String() 75 | _, err := c.Parse([]string{"cmd"}) 76 | assert.Error(t, err) 77 | } 78 | 79 | func TestArgsMultipleRequiredThenNonRequired(t *testing.T) { 80 | c := newTestApp().Writer(ioutil.Discard) 81 | cmd := c.Command("cmd", "") 82 | cmd.Arg("a", "a").Required().String() 83 | cmd.Arg("b", "b").Required().String() 84 | cmd.Arg("c", "c").String() 85 | cmd.Arg("d", "d").String() 86 | _, err := c.Parse([]string{"cmd", "a", "b"}) 87 | assert.NoError(t, err) 88 | _, err = c.Parse([]string{}) 89 | assert.Error(t, err) 90 | } 91 | 92 | func TestDispatchCallbackIsCalled(t *testing.T) { 93 | dispatched := false 94 | c := newTestApp() 95 | c.Command("cmd", "").Action(func(*ParseContext) error { 96 | dispatched = true 97 | return nil 98 | }) 99 | 100 | _, err := c.Parse([]string{"cmd"}) 101 | assert.NoError(t, err) 102 | assert.True(t, dispatched) 103 | } 104 | 105 | func TestTopLevelArgWorks(t *testing.T) { 106 | c := newTestApp() 107 | s := c.Arg("arg", "help").String() 108 | _, err := c.Parse([]string{"foo"}) 109 | assert.NoError(t, err) 110 | assert.Equal(t, "foo", *s) 111 | } 112 | 113 | func TestTopLevelArgCantBeUsedWithCommands(t *testing.T) { 114 | c := newTestApp() 115 | c.Arg("arg", "help").String() 116 | c.Command("cmd", "help") 117 | _, err := c.Parse([]string{}) 118 | assert.Error(t, err) 119 | } 120 | 121 | func TestTooManyArgs(t *testing.T) { 122 | a := newTestApp() 123 | a.Arg("a", "").String() 124 | _, err := a.Parse([]string{"a", "b"}) 125 | assert.Error(t, err) 126 | } 127 | 128 | func TestTooManyArgsAfterCommand(t *testing.T) { 129 | a := newTestApp() 130 | a.Command("a", "") 131 | assert.NoError(t, a.init()) 132 | _, err := a.Parse([]string{"a", "b"}) 133 | assert.Error(t, err) 134 | } 135 | 136 | func TestArgsLooksLikeFlagsWithConsumeRemainder(t *testing.T) { 137 | a := newTestApp() 138 | a.Arg("opts", "").Required().Strings() 139 | _, err := a.Parse([]string{"hello", "-world"}) 140 | assert.Error(t, err) 141 | } 142 | 143 | func TestCommandParseDoesNotResetFlagsToDefault(t *testing.T) { 144 | app := newTestApp() 145 | flag := app.Flag("flag", "").Default("default").String() 146 | app.Command("cmd", "") 147 | 148 | _, err := app.Parse([]string{"--flag=123", "cmd"}) 149 | assert.NoError(t, err) 150 | assert.Equal(t, "123", *flag) 151 | } 152 | 153 | func TestCommandParseDoesNotFailRequired(t *testing.T) { 154 | app := newTestApp() 155 | flag := app.Flag("flag", "").Required().String() 156 | app.Command("cmd", "") 157 | 158 | _, err := app.Parse([]string{"cmd", "--flag=123"}) 159 | assert.NoError(t, err) 160 | assert.Equal(t, "123", *flag) 161 | } 162 | 163 | func TestSelectedCommand(t *testing.T) { 164 | app := newTestApp() 165 | c0 := app.Command("c0", "") 166 | c0.Command("c1", "") 167 | s, err := app.Parse([]string{"c0", "c1"}) 168 | assert.NoError(t, err) 169 | assert.Equal(t, "c0 c1", s) 170 | } 171 | 172 | func TestSubCommandRequired(t *testing.T) { 173 | app := newTestApp() 174 | c0 := app.Command("c0", "") 175 | c0.Command("c1", "") 176 | _, err := app.Parse([]string{"c0"}) 177 | assert.Error(t, err) 178 | } 179 | 180 | func TestInterspersedFalse(t *testing.T) { 181 | app := newTestApp().Interspersed(false) 182 | a1 := app.Arg("a1", "").String() 183 | a2 := app.Arg("a2", "").String() 184 | f1 := app.Flag("flag", "").String() 185 | 186 | _, err := app.Parse([]string{"a1", "--flag=flag"}) 187 | assert.NoError(t, err) 188 | assert.Equal(t, "a1", *a1) 189 | assert.Equal(t, "--flag=flag", *a2) 190 | assert.Equal(t, "", *f1) 191 | } 192 | 193 | func TestInterspersedTrue(t *testing.T) { 194 | // test once with the default value and once with explicit true 195 | for i := 0; i < 2; i++ { 196 | app := newTestApp() 197 | if i != 0 { 198 | t.Log("Setting explicit") 199 | app.Interspersed(true) 200 | } else { 201 | t.Log("Using default") 202 | } 203 | a1 := app.Arg("a1", "").String() 204 | a2 := app.Arg("a2", "").String() 205 | f1 := app.Flag("flag", "").String() 206 | 207 | _, err := app.Parse([]string{"a1", "--flag=flag"}) 208 | assert.NoError(t, err) 209 | assert.Equal(t, "a1", *a1) 210 | assert.Equal(t, "", *a2) 211 | assert.Equal(t, "flag", *f1) 212 | } 213 | } 214 | 215 | func TestDefaultEnvars(t *testing.T) { 216 | a := New("some-app", "").Terminate(nil).DefaultEnvars() 217 | f0 := a.Flag("some-flag", "") 218 | f0.Bool() 219 | f1 := a.Flag("some-other-flag", "").NoEnvar() 220 | f1.Bool() 221 | f2 := a.Flag("a-1-flag", "") 222 | f2.Bool() 223 | _, err := a.Parse([]string{}) 224 | assert.NoError(t, err) 225 | assert.Equal(t, "SOME_APP_SOME_FLAG", f0.envar) 226 | assert.Equal(t, "", f1.envar) 227 | assert.Equal(t, "SOME_APP_A_1_FLAG", f2.envar) 228 | } 229 | 230 | func TestBashCompletionOptionsWithEmptyApp(t *testing.T) { 231 | a := newTestApp() 232 | context, err := a.ParseContext([]string{"--completion-bash"}) 233 | if err != nil { 234 | t.Errorf("Unexpected error whilst parsing context: [%v]", err) 235 | } 236 | args := a.completionOptions(context) 237 | assert.Equal(t, []string(nil), args) 238 | } 239 | 240 | func TestBashCompletionOptions(t *testing.T) { 241 | a := newTestApp() 242 | a.Command("one", "") 243 | a.Flag("flag-0", "").String() 244 | a.Flag("flag-1", "").HintOptions("opt1", "opt2", "opt3").String() 245 | 246 | two := a.Command("two", "") 247 | two.Flag("flag-2", "").String() 248 | two.Flag("flag-3", "").HintOptions("opt4", "opt5", "opt6").String() 249 | 250 | three := a.Command("three", "") 251 | three.Flag("flag-4", "").String() 252 | three.Arg("arg-1", "").String() 253 | three.Arg("arg-2", "").HintOptions("arg-2-opt-1", "arg-2-opt-2").String() 254 | three.Arg("arg-3", "").String() 255 | three.Arg("arg-4", "").HintAction(func() []string { 256 | return []string{"arg-4-opt-1", "arg-4-opt-2"} 257 | }).String() 258 | 259 | cases := []struct { 260 | Args string 261 | ExpectedOptions []string 262 | }{ 263 | { 264 | Args: "--completion-bash", 265 | ExpectedOptions: []string{"help", "one", "three", "two"}, 266 | }, 267 | { 268 | Args: "--completion-bash --", 269 | ExpectedOptions: []string{"--flag-0", "--flag-1", "--help"}, 270 | }, 271 | { 272 | Args: "--completion-bash --fla", 273 | ExpectedOptions: []string{"--flag-0", "--flag-1", "--help"}, 274 | }, 275 | { 276 | // No options available for flag-0, return to cmd completion 277 | Args: "--completion-bash --flag-0", 278 | ExpectedOptions: []string{"help", "one", "three", "two"}, 279 | }, 280 | { 281 | Args: "--completion-bash --flag-0 --", 282 | ExpectedOptions: []string{"--flag-0", "--flag-1", "--help"}, 283 | }, 284 | { 285 | Args: "--completion-bash --flag-1", 286 | ExpectedOptions: []string{"opt1", "opt2", "opt3"}, 287 | }, 288 | { 289 | Args: "--completion-bash --flag-1 opt", 290 | ExpectedOptions: []string{"opt1", "opt2", "opt3"}, 291 | }, 292 | { 293 | Args: "--completion-bash --flag-1 opt1", 294 | ExpectedOptions: []string{"help", "one", "three", "two"}, 295 | }, 296 | { 297 | Args: "--completion-bash --flag-1 opt1 --", 298 | ExpectedOptions: []string{"--flag-0", "--flag-1", "--help"}, 299 | }, 300 | 301 | // Try Subcommand 302 | { 303 | Args: "--completion-bash two", 304 | ExpectedOptions: []string(nil), 305 | }, 306 | { 307 | Args: "--completion-bash two --", 308 | ExpectedOptions: []string{"--help", "--flag-2", "--flag-3", "--flag-0", "--flag-1"}, 309 | }, 310 | { 311 | Args: "--completion-bash two --flag", 312 | ExpectedOptions: []string{"--help", "--flag-2", "--flag-3", "--flag-0", "--flag-1"}, 313 | }, 314 | { 315 | Args: "--completion-bash two --flag-2", 316 | ExpectedOptions: []string(nil), 317 | }, 318 | { 319 | // Top level flags carry downwards 320 | Args: "--completion-bash two --flag-1", 321 | ExpectedOptions: []string{"opt1", "opt2", "opt3"}, 322 | }, 323 | { 324 | // Top level flags carry downwards 325 | Args: "--completion-bash two --flag-1 opt", 326 | ExpectedOptions: []string{"opt1", "opt2", "opt3"}, 327 | }, 328 | { 329 | // Top level flags carry downwards 330 | Args: "--completion-bash two --flag-1 opt1", 331 | ExpectedOptions: []string(nil), 332 | }, 333 | { 334 | Args: "--completion-bash two --flag-3", 335 | ExpectedOptions: []string{"opt4", "opt5", "opt6"}, 336 | }, 337 | { 338 | Args: "--completion-bash two --flag-3 opt", 339 | ExpectedOptions: []string{"opt4", "opt5", "opt6"}, 340 | }, 341 | { 342 | Args: "--completion-bash two --flag-3 opt4", 343 | ExpectedOptions: []string(nil), 344 | }, 345 | { 346 | Args: "--completion-bash two --flag-3 opt4 --", 347 | ExpectedOptions: []string{"--help", "--flag-2", "--flag-3", "--flag-0", "--flag-1"}, 348 | }, 349 | 350 | // Args complete 351 | { 352 | // After a command with an arg with no options, nothing should be 353 | // shown 354 | Args: "--completion-bash three ", 355 | ExpectedOptions: []string(nil), 356 | }, 357 | { 358 | // After a command with an arg, explicitly starting a flag should 359 | // complete flags 360 | Args: "--completion-bash three --", 361 | ExpectedOptions: []string{"--flag-0", "--flag-1", "--flag-4", "--help"}, 362 | }, 363 | { 364 | // After a command with an arg that does have completions, they 365 | // should be shown 366 | Args: "--completion-bash three arg1 ", 367 | ExpectedOptions: []string{"arg-2-opt-1", "arg-2-opt-2"}, 368 | }, 369 | { 370 | // After a command with an arg that does have completions, but a 371 | // flag is started, flag options should be completed 372 | Args: "--completion-bash three arg1 --", 373 | ExpectedOptions: []string{"--flag-0", "--flag-1", "--flag-4", "--help"}, 374 | }, 375 | { 376 | // After a command with an arg that has no completions, and isn't first, 377 | // nothing should be shown 378 | Args: "--completion-bash three arg1 arg2 ", 379 | ExpectedOptions: []string(nil), 380 | }, 381 | { 382 | // After a command with a different arg that also has completions, 383 | // those different options should be shown 384 | Args: "--completion-bash three arg1 arg2 arg3 ", 385 | ExpectedOptions: []string{"arg-4-opt-1", "arg-4-opt-2"}, 386 | }, 387 | { 388 | // After a command with all args listed, nothing should complete 389 | Args: "--completion-bash three arg1 arg2 arg3 arg4", 390 | ExpectedOptions: []string(nil), 391 | }, 392 | } 393 | 394 | for _, c := range cases { 395 | context, _ := a.ParseContext(strings.Split(c.Args, " ")) 396 | args := a.completionOptions(context) 397 | 398 | sort.Strings(args) 399 | sort.Strings(c.ExpectedOptions) 400 | 401 | assert.Equal(t, c.ExpectedOptions, args, "Expected != Actual: [%v] != [%v]. \nInput was: [%v]", c.ExpectedOptions, args, c.Args) 402 | } 403 | 404 | } 405 | -------------------------------------------------------------------------------- /args.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type argGroup struct { 8 | args []*ArgClause 9 | } 10 | 11 | func newArgGroup() *argGroup { 12 | return &argGroup{} 13 | } 14 | 15 | func (a *argGroup) have() bool { 16 | return len(a.args) > 0 17 | } 18 | 19 | // GetArg gets an argument definition. 20 | // 21 | // This allows existing arguments to be modified after definition but before parsing. Useful for 22 | // modular applications. 23 | func (a *argGroup) GetArg(name string) *ArgClause { 24 | for _, arg := range a.args { 25 | if arg.name == name { 26 | return arg 27 | } 28 | } 29 | return nil 30 | } 31 | 32 | func (a *argGroup) Arg(name, help string) *ArgClause { 33 | arg := newArg(name, help) 34 | a.args = append(a.args, arg) 35 | return arg 36 | } 37 | 38 | func (a *argGroup) init() error { 39 | required := 0 40 | seen := map[string]struct{}{} 41 | previousArgMustBeLast := false 42 | for i, arg := range a.args { 43 | if previousArgMustBeLast { 44 | return fmt.Errorf("Args() can't be followed by another argument '%s'", arg.name) 45 | } 46 | if arg.consumesRemainder() { 47 | previousArgMustBeLast = true 48 | } 49 | if _, ok := seen[arg.name]; ok { 50 | return fmt.Errorf("duplicate argument '%s'", arg.name) 51 | } 52 | seen[arg.name] = struct{}{} 53 | if arg.required && required != i { 54 | return fmt.Errorf("required arguments found after non-required") 55 | } 56 | if arg.required { 57 | required++ 58 | } 59 | if err := arg.init(); err != nil { 60 | return err 61 | } 62 | } 63 | return nil 64 | } 65 | 66 | type ArgClause struct { 67 | actionMixin 68 | parserMixin 69 | completionsMixin 70 | envarMixin 71 | name string 72 | help string 73 | defaultValues []string 74 | required bool 75 | } 76 | 77 | func newArg(name, help string) *ArgClause { 78 | a := &ArgClause{ 79 | name: name, 80 | help: help, 81 | } 82 | return a 83 | } 84 | 85 | func (a *ArgClause) setDefault() error { 86 | if a.HasEnvarValue() { 87 | if v, ok := a.value.(remainderArg); !ok || !v.IsCumulative() { 88 | // Use the value as-is 89 | return a.value.Set(a.GetEnvarValue()) 90 | } 91 | for _, value := range a.GetSplitEnvarValue() { 92 | if err := a.value.Set(value); err != nil { 93 | return err 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | if len(a.defaultValues) > 0 { 100 | for _, defaultValue := range a.defaultValues { 101 | if err := a.value.Set(defaultValue); err != nil { 102 | return err 103 | } 104 | } 105 | return nil 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func (a *ArgClause) needsValue() bool { 112 | haveDefault := len(a.defaultValues) > 0 113 | return a.required && !(haveDefault || a.HasEnvarValue()) 114 | } 115 | 116 | func (a *ArgClause) consumesRemainder() bool { 117 | if r, ok := a.value.(remainderArg); ok { 118 | return r.IsCumulative() 119 | } 120 | return false 121 | } 122 | 123 | // Required arguments must be input by the user. They can not have a Default() value provided. 124 | func (a *ArgClause) Required() *ArgClause { 125 | a.required = true 126 | return a 127 | } 128 | 129 | // Default values for this argument. They *must* be parseable by the value of the argument. 130 | func (a *ArgClause) Default(values ...string) *ArgClause { 131 | a.defaultValues = values 132 | return a 133 | } 134 | 135 | // Envar overrides the default value(s) for a flag from an environment variable, 136 | // if it is set. Several default values can be provided by using new lines to 137 | // separate them. 138 | func (a *ArgClause) Envar(name string) *ArgClause { 139 | a.envar = name 140 | a.noEnvar = false 141 | return a 142 | } 143 | 144 | // NoEnvar forces environment variable defaults to be disabled for this flag. 145 | // Most useful in conjunction with app.DefaultEnvars(). 146 | func (a *ArgClause) NoEnvar() *ArgClause { 147 | a.envar = "" 148 | a.noEnvar = true 149 | return a 150 | } 151 | 152 | func (a *ArgClause) Action(action Action) *ArgClause { 153 | a.addAction(action) 154 | return a 155 | } 156 | 157 | func (a *ArgClause) PreAction(action Action) *ArgClause { 158 | a.addPreAction(action) 159 | return a 160 | } 161 | 162 | // HintAction registers a HintAction (function) for the arg to provide completions 163 | func (a *ArgClause) HintAction(action HintAction) *ArgClause { 164 | a.addHintAction(action) 165 | return a 166 | } 167 | 168 | // HintOptions registers any number of options for the flag to provide completions 169 | func (a *ArgClause) HintOptions(options ...string) *ArgClause { 170 | a.addHintAction(func() []string { 171 | return options 172 | }) 173 | return a 174 | } 175 | 176 | func (a *ArgClause) init() error { 177 | if a.required && len(a.defaultValues) > 0 { 178 | return fmt.Errorf("required argument '%s' with unusable default value", a.name) 179 | } 180 | if a.value == nil { 181 | return fmt.Errorf("no parser defined for arg '%s'", a.name) 182 | } 183 | return nil 184 | } 185 | -------------------------------------------------------------------------------- /args_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | "os" 7 | 8 | "github.com/alecthomas/assert" 9 | ) 10 | 11 | func TestArgRemainder(t *testing.T) { 12 | app := New("test", "") 13 | v := app.Arg("test", "").Strings() 14 | args := []string{"hello", "world"} 15 | _, err := app.Parse(args) 16 | assert.NoError(t, err) 17 | assert.Equal(t, args, *v) 18 | } 19 | 20 | func TestArgRemainderErrorsWhenNotLast(t *testing.T) { 21 | a := newArgGroup() 22 | a.Arg("test", "").Strings() 23 | a.Arg("test2", "").String() 24 | assert.Error(t, a.init()) 25 | } 26 | 27 | func TestArgMultipleRequired(t *testing.T) { 28 | terminated := false 29 | app := New("test", "") 30 | app.Version("0.0.0").Writer(ioutil.Discard) 31 | app.Arg("a", "").Required().String() 32 | app.Arg("b", "").Required().String() 33 | app.Terminate(func(int) { terminated = true }) 34 | 35 | _, err := app.Parse([]string{}) 36 | assert.Error(t, err) 37 | _, err = app.Parse([]string{"A"}) 38 | assert.Error(t, err) 39 | _, err = app.Parse([]string{"A", "B"}) 40 | assert.NoError(t, err) 41 | _, err = app.Parse([]string{"--version"}) 42 | assert.True(t, terminated) 43 | } 44 | 45 | func TestInvalidArgsDefaultCanBeOverridden(t *testing.T) { 46 | app := New("test", "") 47 | app.Arg("a", "").Default("invalid").Bool() 48 | _, err := app.Parse([]string{}) 49 | assert.Error(t, err) 50 | } 51 | 52 | func TestArgMultipleValuesDefault(t *testing.T) { 53 | app := New("test", "") 54 | a := app.Arg("a", "").Default("default1", "default2").Strings() 55 | _, err := app.Parse([]string{}) 56 | assert.NoError(t, err) 57 | assert.Equal(t, []string{"default1", "default2"}, *a) 58 | } 59 | 60 | func TestRequiredArgWithEnvarMissingErrors(t *testing.T) { 61 | app := newTestApp() 62 | app.Arg("t", "").Envar("TEST_ARG_ENVAR").Required().Int() 63 | _, err := app.Parse([]string{}) 64 | assert.Error(t, err) 65 | } 66 | 67 | func TestArgRequiredWithEnvar(t *testing.T) { 68 | os.Setenv("TEST_ARG_ENVAR", "123") 69 | app := newTestApp() 70 | flag := app.Arg("t", "").Envar("TEST_ARG_ENVAR").Required().Int() 71 | _, err := app.Parse([]string{}) 72 | assert.NoError(t, err) 73 | assert.Equal(t, 123, *flag) 74 | } 75 | 76 | func TestSubcommandArgRequiredWithEnvar(t *testing.T) { 77 | os.Setenv("TEST_ARG_ENVAR", "123") 78 | app := newTestApp() 79 | cmd := app.Command("command", "") 80 | flag := cmd.Arg("t", "").Envar("TEST_ARG_ENVAR").Required().Int() 81 | _, err := app.Parse([]string{"command"}) 82 | assert.NoError(t, err) 83 | assert.Equal(t, 123, *flag) 84 | } 85 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Example of command usage. 9 | type Example struct { 10 | Usage string 11 | Help string 12 | } 13 | 14 | type cmdMixin struct { 15 | *flagGroup 16 | *argGroup 17 | *cmdGroup 18 | actionMixin 19 | examples []Example 20 | } 21 | 22 | // Example adds an example of the command's usage for help output. 23 | func (c *cmdMixin) Example(usage, help string) *cmdMixin { 24 | c.examples = append(c.examples, Example{ 25 | Usage: usage, 26 | Help: help, 27 | }) 28 | 29 | return c 30 | } 31 | 32 | // Examples returns the usage examples. 33 | func (c *cmdMixin) Examples() []Example { 34 | return c.examples 35 | } 36 | 37 | // CmdCompletion returns completion options for arguments, if that's where 38 | // parsing left off, or commands if there aren't any unsatisfied args. 39 | func (c *cmdMixin) CmdCompletion(context *ParseContext) []string { 40 | var options []string 41 | 42 | // Count args already satisfied - we won't complete those, and add any 43 | // default commands' alternatives, since they weren't listed explicitly 44 | // and the user may want to explicitly list something else. 45 | argsSatisfied := 0 46 | for _, el := range context.Elements { 47 | switch clause := el.Clause.(type) { 48 | case *ArgClause: 49 | if el.Value != nil && *el.Value != "" { 50 | argsSatisfied++ 51 | } 52 | case *Cmd: 53 | options = append(options, clause.completionAlts...) 54 | default: 55 | } 56 | } 57 | 58 | if argsSatisfied < len(c.argGroup.args) { 59 | // Since not all args have been satisfied, show options for the current one 60 | options = append(options, c.argGroup.args[argsSatisfied].resolveCompletions()...) 61 | } else { 62 | // If all args are satisfied, then go back to completing commands 63 | for _, cmd := range c.cmdGroup.commandOrder { 64 | if !cmd.hidden { 65 | options = append(options, cmd.name) 66 | } 67 | } 68 | } 69 | 70 | return options 71 | } 72 | 73 | func (c *cmdMixin) FlagCompletion(flagName string, flagValue string) (choices []string, flagMatch bool, optionMatch bool) { 74 | // Check if flagName matches a known flag. 75 | // If it does, show the options for the flag 76 | // Otherwise, show all flags 77 | 78 | options := []string{} 79 | 80 | for _, flag := range c.flagGroup.flagOrder { 81 | // Loop through each flag and determine if a match exists 82 | if flag.name == flagName { 83 | // User typed entire flag. Need to look for flag options. 84 | options = flag.resolveCompletions() 85 | if len(options) == 0 { 86 | // No Options to Choose From, Assume Match. 87 | return options, true, true 88 | } 89 | 90 | // Loop options to find if the user specified value matches 91 | isPrefix := false 92 | matched := false 93 | 94 | for _, opt := range options { 95 | if flagValue == opt { 96 | matched = true 97 | } else if strings.HasPrefix(opt, flagValue) { 98 | isPrefix = true 99 | } 100 | } 101 | 102 | // Matched Flag Directly 103 | // Flag Value Not Prefixed, and Matched Directly 104 | return options, true, !isPrefix && matched 105 | } 106 | 107 | if !flag.hidden { 108 | options = append(options, "--"+flag.name) 109 | } 110 | } 111 | // No Flag directly matched. 112 | return options, false, false 113 | 114 | } 115 | 116 | type cmdGroup struct { 117 | app *Application 118 | parent *Cmd 119 | commands map[string]*Cmd 120 | commandOrder []*Cmd 121 | } 122 | 123 | func (c *cmdGroup) defaultSubcommand() *Cmd { 124 | for _, cmd := range c.commandOrder { 125 | if cmd.isDefault { 126 | return cmd 127 | } 128 | } 129 | return nil 130 | } 131 | 132 | func (c *cmdGroup) cmdNames() []string { 133 | names := make([]string, 0, len(c.commandOrder)) 134 | for _, cmd := range c.commandOrder { 135 | names = append(names, cmd.name) 136 | } 137 | return names 138 | } 139 | 140 | // GetArg gets a command definition. 141 | // 142 | // This allows existing commands to be modified after definition but before parsing. Useful for 143 | // modular applications. 144 | func (c *cmdGroup) GetCommand(name string) *Cmd { 145 | return c.commands[name] 146 | } 147 | 148 | func newCmdGroup(app *Application) *cmdGroup { 149 | return &cmdGroup{ 150 | app: app, 151 | commands: make(map[string]*Cmd), 152 | } 153 | } 154 | 155 | func (c *cmdGroup) flattenedCommands() (out []*Cmd) { 156 | for _, cmd := range c.commandOrder { 157 | if len(cmd.commands) == 0 { 158 | out = append(out, cmd) 159 | } 160 | out = append(out, cmd.flattenedCommands()...) 161 | } 162 | return 163 | } 164 | 165 | func (c *cmdGroup) addCommand(name, help string) *Cmd { 166 | cmd := newCommand(c.app, name, help) 167 | c.commands[name] = cmd 168 | c.commandOrder = append(c.commandOrder, cmd) 169 | return cmd 170 | } 171 | 172 | func (c *cmdGroup) init() error { 173 | seen := map[string]bool{} 174 | if c.defaultSubcommand() != nil && !c.have() { 175 | return fmt.Errorf("default subcommand %q provided but no subcommands defined", c.defaultSubcommand().name) 176 | } 177 | defaults := []string{} 178 | for _, cmd := range c.commandOrder { 179 | if cmd.isDefault { 180 | defaults = append(defaults, cmd.name) 181 | } 182 | if seen[cmd.name] { 183 | return fmt.Errorf("duplicate command %q", cmd.name) 184 | } 185 | seen[cmd.name] = true 186 | for _, alias := range cmd.aliases { 187 | if seen[alias] { 188 | return fmt.Errorf("alias duplicates existing command %q", alias) 189 | } 190 | c.commands[alias] = cmd 191 | } 192 | if err := cmd.init(); err != nil { 193 | return err 194 | } 195 | } 196 | if len(defaults) > 1 { 197 | return fmt.Errorf("more than one default subcommand exists: %s", strings.Join(defaults, ", ")) 198 | } 199 | return nil 200 | } 201 | 202 | func (c *cmdGroup) have() bool { 203 | return len(c.commands) > 0 204 | } 205 | 206 | type CmdValidator func(*Cmd) error 207 | 208 | // A Cmd is a single top-level command. It encapsulates a set of flags 209 | // and either subcommands or positional arguments. 210 | type Cmd struct { 211 | cmdMixin 212 | app *Application 213 | name string 214 | aliases []string 215 | help string 216 | isDefault bool 217 | validator CmdValidator 218 | hidden bool 219 | completionAlts []string 220 | } 221 | 222 | func newCommand(app *Application, name, help string) *Cmd { 223 | c := &Cmd{ 224 | app: app, 225 | name: name, 226 | help: help, 227 | } 228 | c.flagGroup = newFlagGroup() 229 | c.argGroup = newArgGroup() 230 | c.cmdGroup = newCmdGroup(app) 231 | return c 232 | } 233 | 234 | // Add an Alias for this command. 235 | func (c *Cmd) Alias(name string) *Cmd { 236 | c.aliases = append(c.aliases, name) 237 | return c 238 | } 239 | 240 | // Validate sets a validation function to run when parsing. 241 | func (c *Cmd) Validate(validator CmdValidator) *Cmd { 242 | c.validator = validator 243 | return c 244 | } 245 | 246 | func (c *Cmd) FullCommand() string { 247 | out := []string{c.name} 248 | for p := c.parent; p != nil; p = p.parent { 249 | out = append([]string{p.name}, out...) 250 | } 251 | return strings.Join(out, " ") 252 | } 253 | 254 | // Command adds a new sub-command. 255 | func (c *Cmd) Command(name, help string) *Cmd { 256 | cmd := c.addCommand(name, help) 257 | cmd.parent = c 258 | return cmd 259 | } 260 | 261 | // Default makes this command the default if commands don't match. 262 | func (c *Cmd) Default() *Cmd { 263 | c.isDefault = true 264 | return c 265 | } 266 | 267 | func (c *Cmd) Action(action Action) *Cmd { 268 | c.addAction(action) 269 | return c 270 | } 271 | 272 | func (c *Cmd) PreAction(action Action) *Cmd { 273 | c.addPreAction(action) 274 | return c 275 | } 276 | 277 | func (c *Cmd) init() error { 278 | if err := c.flagGroup.init(c.app.defaultEnvarPrefix()); err != nil { 279 | return err 280 | } 281 | if c.argGroup.have() && c.cmdGroup.have() { 282 | return fmt.Errorf("can't mix Arg()s with Command()s") 283 | } 284 | if err := c.argGroup.init(); err != nil { 285 | return err 286 | } 287 | if err := c.cmdGroup.init(); err != nil { 288 | return err 289 | } 290 | return nil 291 | } 292 | 293 | func (c *Cmd) Hidden() *Cmd { 294 | c.hidden = true 295 | return c 296 | } 297 | -------------------------------------------------------------------------------- /cmd/genvalues/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/alecthomas/template" 10 | ) 11 | 12 | const ( 13 | tmpl = `package kingpin 14 | 15 | // This file is autogenerated by "go generate .". Do not modify. 16 | 17 | {{range .}} 18 | {{if not .NoValueParser}} 19 | // -- {{.Type}} Value 20 | type {{.|ValueName}} struct { v *{{.Type}} } 21 | 22 | func new{{.|Name}}Value(p *{{.Type}}) *{{.|ValueName}} { 23 | return &{{.|ValueName}}{p} 24 | } 25 | 26 | func (f *{{.|ValueName}}) Set(s string) error { 27 | v, err := {{.Parser}} 28 | if err == nil { 29 | *f.v = ({{.Type}})(v) 30 | } 31 | return err 32 | } 33 | 34 | func (f *{{.|ValueName}}) Get() interface{} { return ({{.Type}})(*f.v) } 35 | 36 | func (f *{{.|ValueName}}) String() string { return {{.|Format}} } 37 | 38 | {{if .Help}} 39 | // {{.Help}} 40 | {{else}}\ 41 | // {{.|Name}} parses the next command-line value as {{.Type}}. 42 | {{end}}\ 43 | func (p *parserMixin) {{.|Name}}() (target *{{.Type}}) { 44 | target = new({{.Type}}) 45 | p.{{.|Name}}Var(target) 46 | return 47 | } 48 | 49 | func (p *parserMixin) {{.|Name}}Var(target *{{.Type}}) { 50 | p.SetValue(new{{.|Name}}Value(target)) 51 | } 52 | 53 | {{end}} 54 | // {{.|Plural}} accumulates {{.Type}} values into a slice. 55 | func (p *parserMixin) {{.|Plural}}() (target *[]{{.Type}}) { 56 | target = new([]{{.Type}}) 57 | p.{{.|Plural}}Var(target) 58 | return 59 | } 60 | 61 | func (p *parserMixin) {{.|Plural}}Var(target *[]{{.Type}}) { 62 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 63 | return new{{.|Name}}Value(v.(*{{.Type}})) 64 | })) 65 | } 66 | 67 | {{end}} 68 | ` 69 | ) 70 | 71 | type Value struct { 72 | Name string `json:"name"` 73 | NoValueParser bool `json:"no_value_parser"` 74 | Type string `json:"type"` 75 | Parser string `json:"parser"` 76 | Format string `json:"format"` 77 | Plural string `json:"plural"` 78 | Help string `json:"help"` 79 | } 80 | 81 | func fatalIfError(err error) { 82 | if err != nil { 83 | panic(err) 84 | } 85 | } 86 | 87 | func main() { 88 | r, err := os.Open("values.json") 89 | fatalIfError(err) 90 | defer r.Close() 91 | 92 | v := []Value{} 93 | err = json.NewDecoder(r).Decode(&v) 94 | fatalIfError(err) 95 | 96 | valueName := func(v *Value) string { 97 | if v.Name != "" { 98 | return v.Name 99 | } 100 | return strings.Title(v.Type) 101 | } 102 | 103 | t, err := template.New("genvalues").Funcs(template.FuncMap{ 104 | "Lower": strings.ToLower, 105 | "Format": func(v *Value) string { 106 | if v.Format != "" { 107 | return v.Format 108 | } 109 | return "fmt.Sprintf(\"%v\", *f)" 110 | }, 111 | "ValueName": func(v *Value) string { 112 | name := valueName(v) 113 | return strings.ToLower(name[0:1]) + name[1:] + "Value" 114 | }, 115 | "Name": valueName, 116 | "Plural": func(v *Value) string { 117 | if v.Plural != "" { 118 | return v.Plural 119 | } 120 | return valueName(v) + "List" 121 | }, 122 | }).Parse(tmpl) 123 | fatalIfError(err) 124 | 125 | w, err := os.Create("values_generated.go") 126 | fatalIfError(err) 127 | defer w.Close() 128 | 129 | err = t.Execute(w, v) 130 | fatalIfError(err) 131 | 132 | err = exec.Command("goimports", "-w", "values_generated.go").Run() 133 | fatalIfError(err) 134 | } 135 | -------------------------------------------------------------------------------- /cmd_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/alecthomas/assert" 8 | 9 | "testing" 10 | ) 11 | 12 | func parseAndExecute(app *Application, context *ParseContext) (string, error) { 13 | if err := parse(context, app); err != nil { 14 | return "", err 15 | } 16 | 17 | selected, err := app.setValues(context) 18 | if err != nil { 19 | return "", err 20 | } 21 | 22 | return app.execute(context, selected) 23 | } 24 | 25 | func complete(t *testing.T, app *Application, args ...string) []string { 26 | context, err := app.ParseContext(args) 27 | assert.NoError(t, err) 28 | if err != nil { 29 | return nil 30 | } 31 | 32 | completions := app.completionOptions(context) 33 | sort.Strings(completions) 34 | 35 | return completions 36 | } 37 | 38 | func TestNestedCommands(t *testing.T) { 39 | app := New("app", "") 40 | sub1 := app.Command("sub1", "") 41 | sub1.Flag("sub1", "") 42 | subsub1 := sub1.Command("sub1sub1", "") 43 | subsub1.Command("sub1sub1end", "") 44 | 45 | sub2 := app.Command("sub2", "") 46 | sub2.Flag("sub2", "") 47 | sub2.Command("sub2sub1", "") 48 | 49 | context := tokenize([]string{"sub1", "sub1sub1", "sub1sub1end"}, false) 50 | selected, err := parseAndExecute(app, context) 51 | assert.NoError(t, err) 52 | assert.True(t, context.EOL()) 53 | assert.Equal(t, "sub1 sub1sub1 sub1sub1end", selected) 54 | } 55 | 56 | func TestNestedCommandsWithArgs(t *testing.T) { 57 | app := New("app", "") 58 | cmd := app.Command("a", "").Command("b", "") 59 | a := cmd.Arg("a", "").String() 60 | b := cmd.Arg("b", "").String() 61 | context := tokenize([]string{"a", "b", "c", "d"}, false) 62 | selected, err := parseAndExecute(app, context) 63 | assert.NoError(t, err) 64 | assert.True(t, context.EOL()) 65 | assert.Equal(t, "a b", selected) 66 | assert.Equal(t, "c", *a) 67 | assert.Equal(t, "d", *b) 68 | } 69 | 70 | func TestNestedCommandsWithFlags(t *testing.T) { 71 | app := New("app", "") 72 | cmd := app.Command("a", "").Command("b", "") 73 | a := cmd.Flag("aaa", "").Short('a').String() 74 | b := cmd.Flag("bbb", "").Short('b').String() 75 | err := app.init() 76 | assert.NoError(t, err) 77 | context := tokenize(strings.Split("a b --aaa x -b x", " "), false) 78 | selected, err := parseAndExecute(app, context) 79 | assert.NoError(t, err) 80 | assert.True(t, context.EOL()) 81 | assert.Equal(t, "a b", selected) 82 | assert.Equal(t, "x", *a) 83 | assert.Equal(t, "x", *b) 84 | } 85 | 86 | func TestNestedCommandWithMergedFlags(t *testing.T) { 87 | app := New("app", "") 88 | cmd0 := app.Command("a", "") 89 | cmd0f0 := cmd0.Flag("aflag", "").Bool() 90 | // cmd1 := app.Command("b", "") 91 | // cmd1f0 := cmd0.Flag("bflag", "").Bool() 92 | cmd00 := cmd0.Command("aa", "") 93 | cmd00f0 := cmd00.Flag("aaflag", "").Bool() 94 | err := app.init() 95 | assert.NoError(t, err) 96 | context := tokenize(strings.Split("a aa --aflag --aaflag", " "), false) 97 | selected, err := parseAndExecute(app, context) 98 | assert.NoError(t, err) 99 | assert.True(t, *cmd0f0) 100 | assert.True(t, *cmd00f0) 101 | assert.Equal(t, "a aa", selected) 102 | } 103 | 104 | func TestNestedCommandWithDuplicateFlagErrors(t *testing.T) { 105 | app := New("app", "") 106 | app.Flag("test", "").Bool() 107 | app.Command("cmd0", "").Flag("test", "").Bool() 108 | err := app.init() 109 | assert.Error(t, err) 110 | } 111 | 112 | func TestNestedCommandWithArgAndMergedFlags(t *testing.T) { 113 | app := New("app", "") 114 | cmd0 := app.Command("a", "") 115 | cmd0f0 := cmd0.Flag("aflag", "").Bool() 116 | // cmd1 := app.Command("b", "") 117 | // cmd1f0 := cmd0.Flag("bflag", "").Bool() 118 | cmd00 := cmd0.Command("aa", "") 119 | cmd00a0 := cmd00.Arg("arg", "").String() 120 | cmd00f0 := cmd00.Flag("aaflag", "").Bool() 121 | err := app.init() 122 | assert.NoError(t, err) 123 | context := tokenize(strings.Split("a aa hello --aflag --aaflag", " "), false) 124 | selected, err := parseAndExecute(app, context) 125 | assert.NoError(t, err) 126 | assert.True(t, *cmd0f0) 127 | assert.True(t, *cmd00f0) 128 | assert.Equal(t, "a aa", selected) 129 | assert.Equal(t, "hello", *cmd00a0) 130 | } 131 | 132 | func TestDefaultSubcommandEOL(t *testing.T) { 133 | app := newTestApp() 134 | c0 := app.Command("c0", "").Default() 135 | c0.Command("c01", "").Default() 136 | c0.Command("c02", "") 137 | 138 | cmd, err := app.Parse([]string{"c0"}) 139 | assert.NoError(t, err) 140 | assert.Equal(t, "c0 c01", cmd) 141 | } 142 | 143 | func TestDefaultSubcommandWithArg(t *testing.T) { 144 | app := newTestApp() 145 | c0 := app.Command("c0", "").Default() 146 | c01 := c0.Command("c01", "").Default() 147 | c012 := c01.Command("c012", "").Default() 148 | a0 := c012.Arg("a0", "").String() 149 | c0.Command("c02", "") 150 | 151 | cmd, err := app.Parse([]string{"c0", "hello"}) 152 | assert.NoError(t, err) 153 | assert.Equal(t, "c0 c01 c012", cmd) 154 | assert.Equal(t, "hello", *a0) 155 | } 156 | 157 | func TestDefaultSubcommandWithFlags(t *testing.T) { 158 | app := newTestApp() 159 | c0 := app.Command("c0", "").Default() 160 | _ = c0.Flag("f0", "").Int() 161 | c0c1 := c0.Command("c1", "").Default() 162 | c0c1f1 := c0c1.Flag("f1", "").Int() 163 | selected, err := app.Parse([]string{"--f1=2"}) 164 | assert.NoError(t, err) 165 | assert.Equal(t, "c0 c1", selected) 166 | assert.Equal(t, 2, *c0c1f1) 167 | _, err = app.Parse([]string{"--f2"}) 168 | assert.Error(t, err) 169 | } 170 | 171 | func TestMultipleDefaultCommands(t *testing.T) { 172 | app := newTestApp() 173 | app.Command("c0", "").Default() 174 | app.Command("c1", "").Default() 175 | _, err := app.Parse([]string{}) 176 | assert.Error(t, err) 177 | } 178 | 179 | func TestAliasedCommand(t *testing.T) { 180 | app := newTestApp() 181 | app.Command("one", "").Alias("two") 182 | selected, _ := app.Parse([]string{"one"}) 183 | assert.Equal(t, "one", selected) 184 | selected, _ = app.Parse([]string{"two"}) 185 | assert.Equal(t, "one", selected) 186 | // 2 due to "help" and "one" 187 | assert.Equal(t, 2, len(app.Model().FlattenedCommands())) 188 | } 189 | 190 | func TestDuplicateAlias(t *testing.T) { 191 | app := newTestApp() 192 | app.Command("one", "") 193 | app.Command("two", "").Alias("one") 194 | _, err := app.Parse([]string{"one"}) 195 | assert.Error(t, err) 196 | } 197 | 198 | func TestFlagCompletion(t *testing.T) { 199 | app := newTestApp() 200 | app.Command("one", "") 201 | two := app.Command("two", "") 202 | two.Flag("flag-1", "") 203 | two.Flag("flag-2", "").HintOptions("opt1", "opt2", "opt3") 204 | two.Flag("flag-3", "") 205 | 206 | cases := []struct { 207 | target cmdMixin 208 | flagName string 209 | flagValue string 210 | expectedFlagMatch bool 211 | expectedOptionMatch bool 212 | expectedFlags []string 213 | }{ 214 | { 215 | // Test top level flags 216 | target: app.cmdMixin, 217 | flagName: "", 218 | flagValue: "", 219 | expectedFlagMatch: false, 220 | expectedOptionMatch: false, 221 | expectedFlags: []string{"--help"}, 222 | }, 223 | { 224 | // Test no flag passed 225 | target: two.cmdMixin, 226 | flagName: "", 227 | flagValue: "", 228 | expectedFlagMatch: false, 229 | expectedOptionMatch: false, 230 | expectedFlags: []string{"--flag-1", "--flag-2", "--flag-3"}, 231 | }, 232 | { 233 | // Test an incomplete flag. Should still give all options as if the flag wasn't given at all. 234 | target: two.cmdMixin, 235 | flagName: "flag-", 236 | flagValue: "", 237 | expectedFlagMatch: false, 238 | expectedOptionMatch: false, 239 | expectedFlags: []string{"--flag-1", "--flag-2", "--flag-3"}, 240 | }, 241 | { 242 | // Test with a complete flag. Should show available choices for the flag 243 | // This flag has no options. No options should be produced. 244 | // Should also report an option was matched 245 | target: two.cmdMixin, 246 | flagName: "flag-1", 247 | flagValue: "", 248 | expectedFlagMatch: true, 249 | expectedOptionMatch: true, 250 | expectedFlags: []string(nil), 251 | }, 252 | { 253 | // Test with a complete flag. Should show available choices for the flag 254 | target: two.cmdMixin, 255 | flagName: "flag-2", 256 | flagValue: "", 257 | expectedFlagMatch: true, 258 | expectedOptionMatch: false, 259 | expectedFlags: []string{"opt1", "opt2", "opt3"}, 260 | }, 261 | { 262 | // Test with a complete flag and complete option for that flag. 263 | target: two.cmdMixin, 264 | flagName: "flag-2", 265 | flagValue: "opt1", 266 | expectedFlagMatch: true, 267 | expectedOptionMatch: true, 268 | expectedFlags: []string{"opt1", "opt2", "opt3"}, 269 | }, 270 | } 271 | 272 | for i, c := range cases { 273 | choices, flagMatch, optionMatch := c.target.FlagCompletion(c.flagName, c.flagValue) 274 | assert.Equal(t, c.expectedFlags, choices, "Test case %d: expectedFlags != actual flags", i+1) 275 | assert.Equal(t, c.expectedFlagMatch, flagMatch, "Test case %d: expectedFlagMatch != flagMatch", i+1) 276 | assert.Equal(t, c.expectedOptionMatch, optionMatch, "Test case %d: expectedOptionMatch != optionMatch", i+1) 277 | } 278 | 279 | } 280 | 281 | func TestCmdCompletion(t *testing.T) { 282 | app := newTestApp() 283 | app.Command("one", "") 284 | two := app.Command("two", "") 285 | two.Command("sub1", "") 286 | two.Command("sub2", "") 287 | 288 | assert.Equal(t, []string{"help", "one", "two"}, complete(t, app)) 289 | assert.Equal(t, []string{"sub1", "sub2"}, complete(t, app, "two")) 290 | } 291 | 292 | func TestHiddenCmdCompletion(t *testing.T) { 293 | app := newTestApp() 294 | 295 | // top level visible & hidden cmds, with no sub-cmds 296 | app.Command("visible1", "") 297 | app.Command("hidden1", "").Hidden() 298 | 299 | // visible cmd with visible & hidden sub-cmds 300 | visible2 := app.Command("visible2", "") 301 | visible2.Command("visible2-visible", "") 302 | visible2.Command("visible2-hidden", "").Hidden() 303 | 304 | // hidden cmd with visible & hidden sub-cmds 305 | hidden2 := app.Command("hidden2", "").Hidden() 306 | hidden2.Command("hidden2-visible", "") 307 | hidden2.Command("hidden2-hidden", "").Hidden() 308 | 309 | // Only top level visible cmds should show 310 | assert.Equal(t, []string{"help", "visible1", "visible2"}, complete(t, app)) 311 | 312 | // Only visible sub-cmds should show 313 | assert.Equal(t, []string{"visible2-visible"}, complete(t, app, "visible2")) 314 | 315 | // Hidden commands should still complete visible sub-cmds 316 | assert.Equal(t, []string{"hidden2-visible"}, complete(t, app, "hidden2")) 317 | } 318 | 319 | func TestDefaultCmdCompletion(t *testing.T) { 320 | app := newTestApp() 321 | 322 | cmd1 := app.Command("cmd1", "") 323 | 324 | cmd1Sub1 := cmd1.Command("cmd1-sub1", "") 325 | cmd1Sub1.Arg("cmd1-sub1-arg1", "").HintOptions("cmd1-arg1").String() 326 | 327 | cmd2 := app.Command("cmd2", "").Default() 328 | 329 | cmd2.Command("cmd2-sub1", "") 330 | 331 | cmd2Sub2 := cmd2.Command("cmd2-sub2", "").Default() 332 | 333 | cmd2Sub2Sub1 := cmd2Sub2.Command("cmd2-sub2-sub1", "").Default() 334 | cmd2Sub2Sub1.Arg("cmd2-sub2-sub1-arg1", "").HintOptions("cmd2-sub2-sub1-arg1").String() 335 | cmd2Sub2Sub1.Arg("cmd2-sub2-sub1-arg2", "").HintOptions("cmd2-sub2-sub1-arg2").String() 336 | 337 | // Without args, should get: 338 | // - root cmds (including implicit "help") 339 | // - thread of default cmds 340 | // - first arg hints for the final default cmd 341 | assert.Equal(t, []string{"cmd1", "cmd2", "cmd2-sub1", "cmd2-sub2", "cmd2-sub2-sub1", "cmd2-sub2-sub1-arg1", "help"}, complete(t, app)) 342 | 343 | // With a non-default cmd already listed, should get: 344 | // - sub cmds of that arg 345 | assert.Equal(t, []string{"cmd1-sub1"}, complete(t, app, "cmd1")) 346 | 347 | // With an explicit default cmd listed, should get: 348 | // - default child-cmds 349 | // - first arg hints for the final default cmd 350 | assert.Equal(t, []string{"cmd2-sub1", "cmd2-sub2", "cmd2-sub2-sub1", "cmd2-sub2-sub1-arg1"}, complete(t, app, "cmd2")) 351 | 352 | // Args should be completed when all preceding cmds are explicit, and when 353 | // any of them are implicit (not listed). Check this by trying all possible 354 | // combinations of choosing/excluding the three levels of cmds. This tests 355 | // root-level default, middle default, and end default. 356 | for i := 0; i < 8; i++ { 357 | var cmdline []string 358 | 359 | if i&1 != 0 { 360 | cmdline = append(cmdline, "cmd2") 361 | } 362 | if i&2 != 0 { 363 | cmdline = append(cmdline, "cmd2-sub2") 364 | } 365 | if i&4 != 0 { 366 | cmdline = append(cmdline, "cmd2-sub2-sub1") 367 | } 368 | 369 | assert.Contains(t, complete(t, app, cmdline...), "cmd2-sub2-sub1-arg1", "with cmdline: %v", cmdline) 370 | } 371 | 372 | // With both args of a default sub cmd, should get no completions 373 | assert.Empty(t, complete(t, app, "arg1", "arg2")) 374 | } 375 | -------------------------------------------------------------------------------- /completions.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | // HintAction is a function type who is expected to return a slice of possible 4 | // command line arguments. 5 | type HintAction func() []string 6 | type completionsMixin struct { 7 | hintActions []HintAction 8 | builtinHintActions []HintAction 9 | } 10 | 11 | func (a *completionsMixin) addHintAction(action HintAction) { 12 | a.hintActions = append(a.hintActions, action) 13 | } 14 | 15 | // Allow adding of HintActions which are added internally, ie, EnumVar 16 | func (a *completionsMixin) addHintActionBuiltin(action HintAction) { 17 | a.builtinHintActions = append(a.builtinHintActions, action) 18 | } 19 | 20 | func (a *completionsMixin) resolveCompletions() []string { 21 | var hints []string 22 | 23 | options := a.builtinHintActions 24 | if len(a.hintActions) > 0 { 25 | // User specified their own hintActions. Use those instead. 26 | options = a.hintActions 27 | } 28 | 29 | for _, hintAction := range options { 30 | hints = append(hints, hintAction()...) 31 | } 32 | return hints 33 | } 34 | -------------------------------------------------------------------------------- /completions_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert" 7 | ) 8 | 9 | func TestResolveWithBuiltin(t *testing.T) { 10 | a := completionsMixin{} 11 | 12 | hintAction1 := func() []string { 13 | return []string{"opt1", "opt2"} 14 | } 15 | hintAction2 := func() []string { 16 | return []string{"opt3", "opt4"} 17 | } 18 | 19 | a.builtinHintActions = []HintAction{hintAction1, hintAction2} 20 | 21 | args := a.resolveCompletions() 22 | assert.Equal(t, []string{"opt1", "opt2", "opt3", "opt4"}, args) 23 | } 24 | 25 | func TestResolveWithUser(t *testing.T) { 26 | a := completionsMixin{} 27 | hintAction1 := func() []string { 28 | return []string{"opt1", "opt2"} 29 | } 30 | hintAction2 := func() []string { 31 | return []string{"opt3", "opt4"} 32 | } 33 | 34 | a.hintActions = []HintAction{hintAction1, hintAction2} 35 | 36 | args := a.resolveCompletions() 37 | assert.Equal(t, []string{"opt1", "opt2", "opt3", "opt4"}, args) 38 | } 39 | 40 | func TestResolveWithCombination(t *testing.T) { 41 | a := completionsMixin{} 42 | builtin := func() []string { 43 | return []string{"opt1", "opt2"} 44 | } 45 | user := func() []string { 46 | return []string{"opt3", "opt4"} 47 | } 48 | 49 | a.builtinHintActions = []HintAction{builtin} 50 | a.hintActions = []HintAction{user} 51 | 52 | args := a.resolveCompletions() 53 | // User provided args take preference over builtin (enum-defined) args. 54 | assert.Equal(t, []string{"opt3", "opt4"}, args) 55 | } 56 | 57 | func TestAddHintAction(t *testing.T) { 58 | a := completionsMixin{} 59 | hintFunc := func() []string { 60 | return []string{"opt1", "opt2"} 61 | } 62 | a.addHintAction(hintFunc) 63 | 64 | args := a.resolveCompletions() 65 | assert.Equal(t, []string{"opt1", "opt2"}, args) 66 | } 67 | 68 | func TestAddHintActionBuiltin(t *testing.T) { 69 | a := completionsMixin{} 70 | hintFunc := func() []string { 71 | return []string{"opt1", "opt2"} 72 | } 73 | 74 | a.addHintActionBuiltin(hintFunc) 75 | 76 | args := a.resolveCompletions() 77 | assert.Equal(t, []string{"opt1", "opt2"}, args) 78 | } 79 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package kingpin provides command line interfaces like this: 2 | // 3 | // $ chat 4 | // usage: chat [] [] [ ...] 5 | // 6 | // Flags: 7 | // --debug enable debug mode 8 | // --help Show help. 9 | // --server=127.0.0.1 server address 10 | // 11 | // Commands: 12 | // help 13 | // Show help for a command. 14 | // 15 | // post [] 16 | // Post a message to a channel. 17 | // 18 | // register 19 | // Register a new user. 20 | // 21 | // $ chat help post 22 | // usage: chat [] post [] [] 23 | // 24 | // Post a message to a channel. 25 | // 26 | // Flags: 27 | // --image=IMAGE image to post 28 | // 29 | // Args: 30 | // channel to post to 31 | // [] text to post 32 | // $ chat post --image=~/Downloads/owls.jpg pics 33 | // 34 | // From code like this: 35 | // 36 | // package main 37 | // 38 | // import "gopkg.in/alecthomas/kingpin.v2" 39 | // 40 | // var ( 41 | // debug = kingpin.Flag("debug", "enable debug mode").Default("false").Bool() 42 | // serverIP = kingpin.Flag("server", "server address").Default("127.0.0.1").IP() 43 | // 44 | // register = kingpin.Command("register", "Register a new user.") 45 | // registerNick = register.Arg("nick", "nickname for user").Required().String() 46 | // registerName = register.Arg("name", "name of user").Required().String() 47 | // 48 | // post = kingpin.Command("post", "Post a message to a channel.") 49 | // postImage = post.Flag("image", "image to post").ExistingFile() 50 | // postChannel = post.Arg("channel", "channel to post to").Required().String() 51 | // postText = post.Arg("text", "text to post").String() 52 | // ) 53 | // 54 | // func main() { 55 | // switch kingpin.Parse() { 56 | // // Register user 57 | // case "register": 58 | // println(*registerNick) 59 | // 60 | // // Post message 61 | // case "post": 62 | // if *postImage != nil { 63 | // } 64 | // if *postText != "" { 65 | // } 66 | // } 67 | // } 68 | package kingpin 69 | -------------------------------------------------------------------------------- /envar.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "os" 5 | "regexp" 6 | ) 7 | 8 | var ( 9 | envVarValuesSeparator = "\r?\n" 10 | envVarValuesTrimmer = regexp.MustCompile(envVarValuesSeparator + "$") 11 | envVarValuesSplitter = regexp.MustCompile(envVarValuesSeparator) 12 | ) 13 | 14 | type envarMixin struct { 15 | envar string 16 | noEnvar bool 17 | } 18 | 19 | func (e *envarMixin) HasEnvarValue() bool { 20 | return e.GetEnvarValue() != "" 21 | } 22 | 23 | func (e *envarMixin) GetEnvarValue() string { 24 | if e.noEnvar || e.envar == "" { 25 | return "" 26 | } 27 | return os.Getenv(e.envar) 28 | } 29 | 30 | func (e *envarMixin) GetSplitEnvarValue() []string { 31 | values := make([]string, 0) 32 | 33 | envarValue := e.GetEnvarValue() 34 | if envarValue == "" { 35 | return values 36 | } 37 | 38 | // Split by new line to extract multiple values, if any. 39 | trimmed := envVarValuesTrimmer.ReplaceAllString(envarValue, "") 40 | for _, value := range envVarValuesSplitter.Split(trimmed, -1) { 41 | values = append(values, value) 42 | } 43 | 44 | return values 45 | } 46 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | type HTTPHeaderValue http.Header 10 | 11 | func (h *HTTPHeaderValue) Set(value string) error { 12 | parts := strings.SplitN(value, ":", 2) 13 | if len(parts) != 2 { 14 | return fmt.Errorf("expected HEADER:VALUE got '%s'", value) 15 | } 16 | (*http.Header)(h).Add(parts[0], parts[1]) 17 | return nil 18 | } 19 | 20 | func (h *HTTPHeaderValue) Get() interface{} { 21 | return (http.Header)(*h) 22 | } 23 | 24 | func (h *HTTPHeaderValue) String() string { 25 | return "" 26 | } 27 | 28 | func HTTPHeader(s Settings) (target *http.Header) { 29 | target = new(http.Header) 30 | s.SetValue((*HTTPHeaderValue)(target)) 31 | return 32 | } 33 | 34 | // This example ilustrates how to define custom parsers. HTTPHeader 35 | // cumulatively parses each encountered --header flag into a http.Header struct. 36 | func ExampleValue() { 37 | var ( 38 | curl = New("curl", "transfer a URL") 39 | headers = HTTPHeader(curl.Flag("headers", "Add HTTP headers to the request.").Short('H').PlaceHolder("HEADER:VALUE")) 40 | ) 41 | 42 | curl.Parse([]string{"-H Content-Type:application/octet-stream"}) 43 | for key, value := range *headers { 44 | fmt.Printf("%s = %s\n", key, value) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type flagGroup struct { 9 | short map[string]*FlagClause 10 | long map[string]*FlagClause 11 | flagOrder []*FlagClause 12 | } 13 | 14 | func newFlagGroup() *flagGroup { 15 | return &flagGroup{ 16 | short: map[string]*FlagClause{}, 17 | long: map[string]*FlagClause{}, 18 | } 19 | } 20 | 21 | // GetFlag gets a flag definition. 22 | // 23 | // This allows existing flags to be modified after definition but before parsing. Useful for 24 | // modular applications. 25 | func (f *flagGroup) GetFlag(name string) *FlagClause { 26 | return f.long[name] 27 | } 28 | 29 | // Flag defines a new flag with the given long name and help. 30 | func (f *flagGroup) Flag(name, help string) *FlagClause { 31 | flag := newFlag(name, help) 32 | f.long[name] = flag 33 | f.flagOrder = append(f.flagOrder, flag) 34 | return flag 35 | } 36 | 37 | func (f *flagGroup) init(defaultEnvarPrefix string) error { 38 | if err := f.checkDuplicates(); err != nil { 39 | return err 40 | } 41 | for _, flag := range f.long { 42 | if defaultEnvarPrefix != "" && !flag.noEnvar && flag.envar == "" { 43 | flag.envar = envarTransform(defaultEnvarPrefix + "_" + flag.name) 44 | } 45 | if err := flag.init(); err != nil { 46 | return err 47 | } 48 | if flag.shorthand != 0 { 49 | f.short[string(flag.shorthand)] = flag 50 | } 51 | } 52 | return nil 53 | } 54 | 55 | func (f *flagGroup) checkDuplicates() error { 56 | seenShort := map[rune]bool{} 57 | seenLong := map[string]bool{} 58 | for _, flag := range f.flagOrder { 59 | if flag.shorthand != 0 { 60 | if _, ok := seenShort[flag.shorthand]; ok { 61 | return fmt.Errorf("duplicate short flag -%c", flag.shorthand) 62 | } 63 | seenShort[flag.shorthand] = true 64 | } 65 | if _, ok := seenLong[flag.name]; ok { 66 | return fmt.Errorf("duplicate long flag --%s", flag.name) 67 | } 68 | seenLong[flag.name] = true 69 | } 70 | return nil 71 | } 72 | 73 | func (f *flagGroup) parse(context *ParseContext) (*FlagClause, error) { 74 | var token *Token 75 | 76 | loop: 77 | for { 78 | token = context.Peek() 79 | switch token.Type { 80 | case TokenEOL: 81 | break loop 82 | 83 | case TokenLong, TokenShort: 84 | flagToken := token 85 | defaultValue := "" 86 | var flag *FlagClause 87 | var ok bool 88 | invert := false 89 | 90 | name := token.Value 91 | if token.Type == TokenLong { 92 | flag, ok = f.long[name] 93 | if !ok { 94 | if strings.HasPrefix(name, "no-") { 95 | name = name[3:] 96 | invert = true 97 | } 98 | flag, ok = f.long[name] 99 | } 100 | if !ok { 101 | return nil, fmt.Errorf("unknown long flag '%s'", flagToken) 102 | } 103 | } else { 104 | flag, ok = f.short[name] 105 | if !ok { 106 | return nil, fmt.Errorf("unknown short flag '%s'", flagToken) 107 | } 108 | } 109 | 110 | context.Next() 111 | 112 | fb, ok := flag.value.(boolFlag) 113 | if ok && fb.IsBoolFlag() { 114 | if invert { 115 | defaultValue = "false" 116 | } else { 117 | defaultValue = "true" 118 | } 119 | } else { 120 | if invert { 121 | context.Push(token) 122 | return nil, fmt.Errorf("unknown long flag '%s'", flagToken) 123 | } 124 | token = context.Peek() 125 | if token.Type != TokenArg { 126 | context.Push(token) 127 | return nil, fmt.Errorf("expected argument for flag '%s'", flagToken) 128 | } 129 | context.Next() 130 | defaultValue = token.Value 131 | } 132 | 133 | context.matchedFlag(flag, defaultValue) 134 | return flag, nil 135 | 136 | default: 137 | break loop 138 | } 139 | } 140 | return nil, nil 141 | } 142 | 143 | // FlagClause is a fluid interface used to build flags. 144 | type FlagClause struct { 145 | parserMixin 146 | actionMixin 147 | completionsMixin 148 | envarMixin 149 | name string 150 | shorthand rune 151 | help string 152 | defaultValues []string 153 | placeholder string 154 | hidden bool 155 | } 156 | 157 | func newFlag(name, help string) *FlagClause { 158 | f := &FlagClause{ 159 | name: name, 160 | help: help, 161 | } 162 | return f 163 | } 164 | 165 | func (f *FlagClause) setDefault() error { 166 | if f.HasEnvarValue() { 167 | if v, ok := f.value.(repeatableFlag); !ok || !v.IsCumulative() { 168 | // Use the value as-is 169 | return f.value.Set(f.GetEnvarValue()) 170 | } else { 171 | for _, value := range f.GetSplitEnvarValue() { 172 | if err := f.value.Set(value); err != nil { 173 | return err 174 | } 175 | } 176 | return nil 177 | } 178 | } 179 | 180 | if len(f.defaultValues) > 0 { 181 | for _, defaultValue := range f.defaultValues { 182 | if err := f.value.Set(defaultValue); err != nil { 183 | return err 184 | } 185 | } 186 | return nil 187 | } 188 | 189 | return nil 190 | } 191 | 192 | func (f *FlagClause) needsValue() bool { 193 | haveDefault := len(f.defaultValues) > 0 194 | return f.required && !(haveDefault || f.HasEnvarValue()) 195 | } 196 | 197 | func (f *FlagClause) init() error { 198 | if f.required && len(f.defaultValues) > 0 { 199 | return fmt.Errorf("required flag '--%s' with default value that will never be used", f.name) 200 | } 201 | if f.value == nil { 202 | return fmt.Errorf("no type defined for --%s (eg. .String())", f.name) 203 | } 204 | if v, ok := f.value.(repeatableFlag); (!ok || !v.IsCumulative()) && len(f.defaultValues) > 1 { 205 | return fmt.Errorf("invalid default for '--%s', expecting single value", f.name) 206 | } 207 | return nil 208 | } 209 | 210 | // Dispatch to the given function after the flag is parsed and validated. 211 | func (f *FlagClause) Action(action Action) *FlagClause { 212 | f.addAction(action) 213 | return f 214 | } 215 | 216 | func (f *FlagClause) PreAction(action Action) *FlagClause { 217 | f.addPreAction(action) 218 | return f 219 | } 220 | 221 | // HintAction registers a HintAction (function) for the flag to provide completions 222 | func (a *FlagClause) HintAction(action HintAction) *FlagClause { 223 | a.addHintAction(action) 224 | return a 225 | } 226 | 227 | // HintOptions registers any number of options for the flag to provide completions 228 | func (a *FlagClause) HintOptions(options ...string) *FlagClause { 229 | a.addHintAction(func() []string { 230 | return options 231 | }) 232 | return a 233 | } 234 | 235 | func (a *FlagClause) EnumVar(target *string, options ...string) { 236 | a.parserMixin.EnumVar(target, options...) 237 | a.addHintActionBuiltin(func() []string { 238 | return options 239 | }) 240 | } 241 | 242 | func (a *FlagClause) Enum(options ...string) (target *string) { 243 | a.addHintActionBuiltin(func() []string { 244 | return options 245 | }) 246 | return a.parserMixin.Enum(options...) 247 | } 248 | 249 | // Default values for this flag. They *must* be parseable by the value of the flag. 250 | func (f *FlagClause) Default(values ...string) *FlagClause { 251 | f.defaultValues = values 252 | return f 253 | } 254 | 255 | // DEPRECATED: Use Envar(name) instead. 256 | func (f *FlagClause) OverrideDefaultFromEnvar(envar string) *FlagClause { 257 | return f.Envar(envar) 258 | } 259 | 260 | // Envar overrides the default value(s) for a flag from an environment variable, 261 | // if it is set. Several default values can be provided by using new lines to 262 | // separate them. 263 | func (f *FlagClause) Envar(name string) *FlagClause { 264 | f.envar = name 265 | f.noEnvar = false 266 | return f 267 | } 268 | 269 | // NoEnvar forces environment variable defaults to be disabled for this flag. 270 | // Most useful in conjunction with app.DefaultEnvars(). 271 | func (f *FlagClause) NoEnvar() *FlagClause { 272 | f.envar = "" 273 | f.noEnvar = true 274 | return f 275 | } 276 | 277 | // PlaceHolder sets the place-holder string used for flag values in the help. The 278 | // default behaviour is to use the value provided by Default() if provided, 279 | // then fall back on the capitalized flag name. 280 | func (f *FlagClause) PlaceHolder(placeholder string) *FlagClause { 281 | f.placeholder = placeholder 282 | return f 283 | } 284 | 285 | // Hidden hides a flag from usage but still allows it to be used. 286 | func (f *FlagClause) Hidden() *FlagClause { 287 | f.hidden = true 288 | return f 289 | } 290 | 291 | // Required makes the flag required. You can not provide a Default() value to a Required() flag. 292 | func (f *FlagClause) Required() *FlagClause { 293 | f.required = true 294 | return f 295 | } 296 | 297 | // Short sets the short flag name. 298 | func (f *FlagClause) Short(name rune) *FlagClause { 299 | f.shorthand = name 300 | return f 301 | } 302 | 303 | // Bool makes this flag a boolean flag. 304 | func (f *FlagClause) Bool() (target *bool) { 305 | target = new(bool) 306 | f.SetValue(newBoolValue(target)) 307 | return 308 | } 309 | -------------------------------------------------------------------------------- /flags_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/alecthomas/assert" 8 | 9 | "testing" 10 | ) 11 | 12 | func TestBool(t *testing.T) { 13 | app := newTestApp() 14 | b := app.Flag("b", "").Bool() 15 | _, err := app.Parse([]string{"--b"}) 16 | assert.NoError(t, err) 17 | assert.True(t, *b) 18 | } 19 | 20 | func TestNoBool(t *testing.T) { 21 | fg := newFlagGroup() 22 | f := fg.Flag("b", "").Default("true") 23 | b := f.Bool() 24 | fg.init("") 25 | tokens := tokenize([]string{"--no-b"}, false) 26 | _, err := fg.parse(tokens) 27 | assert.NoError(t, err) 28 | assert.False(t, *b) 29 | } 30 | 31 | func TestNegateNonBool(t *testing.T) { 32 | fg := newFlagGroup() 33 | f := fg.Flag("b", "") 34 | f.Int() 35 | fg.init("") 36 | tokens := tokenize([]string{"--no-b"}, false) 37 | _, err := fg.parse(tokens) 38 | assert.Error(t, err) 39 | } 40 | 41 | func TestNegativePrefixLongFlag(t *testing.T) { 42 | fg := newFlagGroup() 43 | f := fg.Flag("no-comment", "") 44 | b := f.Bool() 45 | fg.init("") 46 | tokens := tokenize([]string{"--no-comment"}, false) 47 | _, err := fg.parse(tokens) 48 | assert.NoError(t, err) 49 | assert.False(t, *b) 50 | } 51 | 52 | func TestInvalidFlagDefaultCanBeOverridden(t *testing.T) { 53 | app := newTestApp() 54 | app.Flag("a", "").Default("invalid").Bool() 55 | _, err := app.Parse([]string{}) 56 | assert.Error(t, err) 57 | } 58 | 59 | func TestRequiredFlag(t *testing.T) { 60 | app := newTestApp() 61 | app.Version("0.0.0").Writer(ioutil.Discard) 62 | exits := 0 63 | app.Terminate(func(int) { exits++ }) 64 | app.Flag("a", "").Required().Bool() 65 | _, err := app.Parse([]string{"--a"}) 66 | assert.NoError(t, err) 67 | _, err = app.Parse([]string{}) 68 | assert.Error(t, err) 69 | _, err = app.Parse([]string{"--version"}) 70 | assert.Equal(t, 1, exits) 71 | } 72 | 73 | func TestShortFlag(t *testing.T) { 74 | app := newTestApp() 75 | f := app.Flag("long", "").Short('s').Bool() 76 | _, err := app.Parse([]string{"-s"}) 77 | assert.NoError(t, err) 78 | assert.True(t, *f) 79 | } 80 | 81 | func TestUnicodeShortFlag(t *testing.T) { 82 | app := newTestApp() 83 | f := app.Flag("aaa", "").Short('ä').Bool() 84 | _, err := app.Parse([]string{"-ä"}) 85 | assert.NoError(t, err) 86 | assert.True(t, *f) 87 | } 88 | 89 | func TestCombinedShortFlags(t *testing.T) { 90 | app := newTestApp() 91 | a := app.Flag("short0", "").Short('0').Bool() 92 | b := app.Flag("short1", "").Short('1').Bool() 93 | c := app.Flag("short2", "").Short('2').Bool() 94 | _, err := app.Parse([]string{"-01"}) 95 | assert.NoError(t, err) 96 | assert.True(t, *a) 97 | assert.True(t, *b) 98 | assert.False(t, *c) 99 | } 100 | 101 | func TestCombinedUnicodeShortFlags(t *testing.T) { 102 | app := newTestApp() 103 | a := app.Flag("short0", "").Short('0').Bool() 104 | b := app.Flag("short1", "").Short('1').Bool() 105 | c := app.Flag("short2", "").Short('ä').Bool() 106 | d := app.Flag("short3", "").Short('2').Bool() 107 | _, err := app.Parse([]string{"-0ä1"}) 108 | assert.NoError(t, err) 109 | assert.True(t, *a) 110 | assert.True(t, *b) 111 | assert.True(t, *c) 112 | assert.False(t, *d) 113 | } 114 | 115 | func TestCombinedShortFlagArg(t *testing.T) { 116 | a := newTestApp() 117 | n := a.Flag("short", "").Short('s').Int() 118 | _, err := a.Parse([]string{"-s10"}) 119 | assert.NoError(t, err) 120 | assert.Equal(t, 10, *n) 121 | } 122 | 123 | func TestCombinedUnicodeShortFlagArg(t *testing.T) { 124 | app := newTestApp() 125 | a := app.Flag("short", "").Short('ä').Int() 126 | _, err := app.Parse([]string{"-ä10"}) 127 | assert.NoError(t, err) 128 | assert.Equal(t, 10, *a) 129 | } 130 | 131 | func TestCombinedUnicodeShortFlagUnicodeArg(t *testing.T) { 132 | app := newTestApp() 133 | a := app.Flag("short", "").Short('ä').String() 134 | _, err := app.Parse([]string{"-äöö"}) 135 | assert.NoError(t, err) 136 | assert.Equal(t, "öö", *a) 137 | } 138 | 139 | func TestEmptyShortFlagIsAnError(t *testing.T) { 140 | _, err := newTestApp().Parse([]string{"-"}) 141 | assert.Error(t, err) 142 | } 143 | 144 | func TestRequiredWithEnvarMissingErrors(t *testing.T) { 145 | app := newTestApp() 146 | app.Flag("t", "").OverrideDefaultFromEnvar("TEST_ENVAR").Required().Int() 147 | _, err := app.Parse([]string{}) 148 | assert.Error(t, err) 149 | } 150 | 151 | func TestRequiredWithEnvar(t *testing.T) { 152 | os.Setenv("TEST_ENVAR", "123") 153 | app := newTestApp() 154 | flag := app.Flag("t", "").Envar("TEST_ENVAR").Required().Int() 155 | _, err := app.Parse([]string{}) 156 | assert.NoError(t, err) 157 | assert.Equal(t, 123, *flag) 158 | } 159 | 160 | func TestSubcommandFlagRequiredWithEnvar(t *testing.T) { 161 | os.Setenv("TEST_ENVAR", "123") 162 | app := newTestApp() 163 | cmd := app.Command("command", "") 164 | flag := cmd.Flag("t", "").Envar("TEST_ENVAR").Required().Int() 165 | _, err := app.Parse([]string{"command"}) 166 | assert.NoError(t, err) 167 | assert.Equal(t, 123, *flag) 168 | } 169 | 170 | func TestRegexp(t *testing.T) { 171 | app := newTestApp() 172 | flag := app.Flag("reg", "").Regexp() 173 | _, err := app.Parse([]string{"--reg", "^abc$"}) 174 | assert.NoError(t, err) 175 | assert.NotNil(t, *flag) 176 | assert.Equal(t, "^abc$", (*flag).String()) 177 | assert.Regexp(t, *flag, "abc") 178 | assert.NotRegexp(t, *flag, "abcd") 179 | } 180 | 181 | func TestDuplicateShortFlag(t *testing.T) { 182 | app := newTestApp() 183 | app.Flag("a", "").Short('a').String() 184 | app.Flag("b", "").Short('a').String() 185 | _, err := app.Parse([]string{}) 186 | assert.Error(t, err) 187 | } 188 | 189 | func TestDuplicateLongFlag(t *testing.T) { 190 | app := newTestApp() 191 | app.Flag("a", "").String() 192 | app.Flag("a", "").String() 193 | _, err := app.Parse([]string{}) 194 | assert.Error(t, err) 195 | } 196 | 197 | func TestGetFlagAndOverrideDefault(t *testing.T) { 198 | app := newTestApp() 199 | a := app.Flag("a", "").Default("default").String() 200 | _, err := app.Parse([]string{}) 201 | assert.NoError(t, err) 202 | assert.Equal(t, "default", *a) 203 | app.GetFlag("a").Default("new") 204 | _, err = app.Parse([]string{}) 205 | assert.NoError(t, err) 206 | assert.Equal(t, "new", *a) 207 | } 208 | 209 | func TestEnvarOverrideDefault(t *testing.T) { 210 | os.Setenv("TEST_ENVAR", "123") 211 | app := newTestApp() 212 | flag := app.Flag("t", "").Default("default").Envar("TEST_ENVAR").String() 213 | _, err := app.Parse([]string{}) 214 | assert.NoError(t, err) 215 | assert.Equal(t, "123", *flag) 216 | } 217 | 218 | func TestFlagMultipleValuesDefault(t *testing.T) { 219 | app := newTestApp() 220 | a := app.Flag("a", "").Default("default1", "default2").Strings() 221 | _, err := app.Parse([]string{}) 222 | assert.NoError(t, err) 223 | assert.Equal(t, []string{"default1", "default2"}, *a) 224 | } 225 | 226 | func TestFlagMultipleValuesDefaultNonRepeatable(t *testing.T) { 227 | c := newTestApp() 228 | c.Flag("foo", "foo").Default("a", "b").String() 229 | _, err := c.Parse([]string{}) 230 | assert.Error(t, err) 231 | } 232 | 233 | func TestFlagMultipleValuesDefaultEnvarUnix(t *testing.T) { 234 | app := newTestApp() 235 | a := app.Flag("a", "").Envar("TEST_MULTIPLE_VALUES").Strings() 236 | os.Setenv("TEST_MULTIPLE_VALUES", "123\n456\n") 237 | _, err := app.Parse([]string{}) 238 | assert.NoError(t, err) 239 | assert.Equal(t, []string{"123", "456"}, *a) 240 | } 241 | 242 | func TestFlagMultipleValuesDefaultEnvarWindows(t *testing.T) { 243 | app := newTestApp() 244 | a := app.Flag("a", "").Envar("TEST_MULTIPLE_VALUES").Strings() 245 | os.Setenv("TEST_MULTIPLE_VALUES", "123\r\n456\r\n") 246 | _, err := app.Parse([]string{}) 247 | assert.NoError(t, err) 248 | assert.Equal(t, []string{"123", "456"}, *a) 249 | } 250 | 251 | func TestFlagMultipleValuesDefaultEnvarNonRepeatable(t *testing.T) { 252 | c := newTestApp() 253 | a := c.Flag("foo", "foo").Envar("TEST_MULTIPLE_VALUES_NON_REPEATABLE").String() 254 | os.Setenv("TEST_MULTIPLE_VALUES_NON_REPEATABLE", "123\n456") 255 | _, err := c.Parse([]string{}) 256 | assert.NoError(t, err) 257 | assert.Equal(t, "123\n456", *a) 258 | } 259 | 260 | func TestFlagHintAction(t *testing.T) { 261 | c := newTestApp() 262 | 263 | action := func() []string { 264 | return []string{"opt1", "opt2"} 265 | } 266 | 267 | a := c.Flag("foo", "foo").HintAction(action) 268 | args := a.resolveCompletions() 269 | assert.Equal(t, []string{"opt1", "opt2"}, args) 270 | } 271 | 272 | func TestFlagHintOptions(t *testing.T) { 273 | c := newTestApp() 274 | 275 | a := c.Flag("foo", "foo").HintOptions("opt1", "opt2") 276 | args := a.resolveCompletions() 277 | assert.Equal(t, []string{"opt1", "opt2"}, args) 278 | } 279 | 280 | func TestFlagEnumVar(t *testing.T) { 281 | c := newTestApp() 282 | var bar string 283 | 284 | a := c.Flag("foo", "foo") 285 | a.Enum("opt1", "opt2") 286 | b := c.Flag("bar", "bar") 287 | b.EnumVar(&bar, "opt3", "opt4") 288 | 289 | args := a.resolveCompletions() 290 | assert.Equal(t, []string{"opt1", "opt2"}, args) 291 | 292 | args = b.resolveCompletions() 293 | assert.Equal(t, []string{"opt3", "opt4"}, args) 294 | } 295 | 296 | func TestMultiHintOptions(t *testing.T) { 297 | c := newTestApp() 298 | 299 | a := c.Flag("foo", "foo").HintOptions("opt1").HintOptions("opt2") 300 | args := a.resolveCompletions() 301 | assert.Equal(t, []string{"opt1", "opt2"}, args) 302 | } 303 | func TestMultiHintActions(t *testing.T) { 304 | c := newTestApp() 305 | 306 | a := c.Flag("foo", "foo"). 307 | HintAction(func() []string { 308 | return []string{"opt1"} 309 | }). 310 | HintAction(func() []string { 311 | return []string{"opt2"} 312 | }) 313 | args := a.resolveCompletions() 314 | assert.Equal(t, []string{"opt1", "opt2"}, args) 315 | } 316 | 317 | func TestCombinationHintActionsOptions(t *testing.T) { 318 | c := newTestApp() 319 | 320 | a := c.Flag("foo", "foo").HintAction(func() []string { 321 | return []string{"opt1"} 322 | }).HintOptions("opt2") 323 | args := a.resolveCompletions() 324 | assert.Equal(t, []string{"opt1", "opt2"}, args) 325 | } 326 | 327 | func TestCombinationEnumActions(t *testing.T) { 328 | c := newTestApp() 329 | var foo string 330 | 331 | a := c.Flag("foo", "foo"). 332 | HintAction(func() []string { 333 | return []string{"opt1", "opt2"} 334 | }) 335 | a.Enum("opt3", "opt4") 336 | 337 | b := c.Flag("bar", "bar"). 338 | HintAction(func() []string { 339 | return []string{"opt5", "opt6"} 340 | }) 341 | b.EnumVar(&foo, "opt3", "opt4") 342 | 343 | // Provided HintActions should override automatically generated Enum options. 344 | args := a.resolveCompletions() 345 | assert.Equal(t, []string{"opt1", "opt2"}, args) 346 | 347 | args = b.resolveCompletions() 348 | assert.Equal(t, []string{"opt5", "opt6"}, args) 349 | } 350 | 351 | func TestCombinationEnumOptions(t *testing.T) { 352 | c := newTestApp() 353 | var foo string 354 | 355 | a := c.Flag("foo", "foo").HintOptions("opt1", "opt2") 356 | a.Enum("opt3", "opt4") 357 | 358 | b := c.Flag("bar", "bar").HintOptions("opt5", "opt6") 359 | b.EnumVar(&foo, "opt3", "opt4") 360 | 361 | // Provided HintOptions should override automatically generated Enum options. 362 | args := a.resolveCompletions() 363 | assert.Equal(t, []string{"opt1", "opt2"}, args) 364 | 365 | args = b.resolveCompletions() 366 | assert.Equal(t, []string{"opt5", "opt6"}, args) 367 | 368 | } 369 | -------------------------------------------------------------------------------- /global.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | var ( 9 | // CommandLine is the default Kingpin parser. 10 | CommandLine = New(filepath.Base(os.Args[0]), "") 11 | // Global help flag. Exposed for user customisation. 12 | HelpFlag = CommandLine.HelpFlag 13 | // Top-level help command. Exposed for user customisation. May be nil. 14 | HelpCommand = CommandLine.HelpCommand 15 | // Global version flag. Exposed for user customisation. May be nil. 16 | VersionFlag = CommandLine.VersionFlag 17 | ) 18 | 19 | // Command adds a new command to the default parser. 20 | func Command(name, help string) *Cmd { 21 | return CommandLine.Command(name, help) 22 | } 23 | 24 | // Flag adds a new flag to the default parser. 25 | func Flag(name, help string) *FlagClause { 26 | return CommandLine.Flag(name, help) 27 | } 28 | 29 | // Arg adds a new argument to the top-level of the default parser. 30 | func Arg(name, help string) *ArgClause { 31 | return CommandLine.Arg(name, help) 32 | } 33 | 34 | // Parse and return the selected command. Will call the termination handler if 35 | // an error is encountered. 36 | func Parse() string { 37 | selected := MustParse(CommandLine.Parse(os.Args[1:])) 38 | if selected == "" && CommandLine.cmdGroup.have() { 39 | Usage() 40 | CommandLine.terminate(0) 41 | } 42 | return selected 43 | } 44 | 45 | // Errorf prints an error message to stderr. 46 | func Errorf(format string, args ...interface{}) { 47 | CommandLine.Errorf(format, args...) 48 | } 49 | 50 | // Fatalf prints an error message to stderr and exits. 51 | func Fatalf(format string, args ...interface{}) { 52 | CommandLine.Fatalf(format, args...) 53 | } 54 | 55 | // FatalIfError prints an error and exits if err is not nil. The error is printed 56 | // with the given prefix. 57 | func FatalIfError(err error, format string, args ...interface{}) { 58 | CommandLine.FatalIfError(err, format, args...) 59 | } 60 | 61 | // FatalUsage prints an error message followed by usage information, then 62 | // exits with a non-zero status. 63 | func FatalUsage(format string, args ...interface{}) { 64 | CommandLine.FatalUsage(format, args...) 65 | } 66 | 67 | // FatalUsageContext writes a printf formatted error message to stderr, then 68 | // usage information for the given ParseContext, before exiting. 69 | func FatalUsageContext(context *ParseContext, format string, args ...interface{}) { 70 | CommandLine.FatalUsageContext(context, format, args...) 71 | } 72 | 73 | // Usage prints usage to stderr. 74 | func Usage() { 75 | CommandLine.Usage(os.Args[1:]) 76 | } 77 | 78 | // Set global usage template to use (defaults to DefaultUsageTemplate). 79 | func UsageTemplate(template string) *Application { 80 | return CommandLine.UsageTemplate(template) 81 | } 82 | 83 | // MustParse can be used with app.Parse(args) to exit with an error if parsing fails. 84 | func MustParse(command string, err error) string { 85 | if err != nil { 86 | Fatalf("%s\n", err) 87 | } 88 | return command 89 | } 90 | 91 | // Version adds a flag for displaying the application version number. 92 | func Version(version string) *Application { 93 | return CommandLine.Version(version) 94 | } 95 | -------------------------------------------------------------------------------- /guesswidth.go: -------------------------------------------------------------------------------- 1 | // +build appengine !linux,!freebsd,!darwin,!dragonfly,!netbsd,!openbsd 2 | 3 | package kingpin 4 | 5 | import "io" 6 | 7 | func guessWidth(w io.Writer) int { 8 | return 80 9 | } 10 | -------------------------------------------------------------------------------- /guesswidth_unix.go: -------------------------------------------------------------------------------- 1 | // +build !appengine,linux freebsd darwin dragonfly netbsd openbsd 2 | 3 | package kingpin 4 | 5 | import ( 6 | "io" 7 | "os" 8 | "strconv" 9 | "syscall" 10 | "unsafe" 11 | ) 12 | 13 | func guessWidth(w io.Writer) int { 14 | // check if COLUMNS env is set to comply with 15 | // http://pubs.opengroup.org/onlinepubs/009604499/basedefs/xbd_chap08.html 16 | colsStr := os.Getenv("COLUMNS") 17 | if colsStr != "" { 18 | if cols, err := strconv.Atoi(colsStr); err == nil { 19 | return cols 20 | } 21 | } 22 | 23 | if t, ok := w.(*os.File); ok { 24 | fd := t.Fd() 25 | var dimensions [4]uint16 26 | 27 | if _, _, err := syscall.Syscall6( 28 | syscall.SYS_IOCTL, 29 | uintptr(fd), 30 | uintptr(syscall.TIOCGWINSZ), 31 | uintptr(unsafe.Pointer(&dimensions)), 32 | 0, 0, 0, 33 | ); err == 0 { 34 | return int(dimensions[1]) 35 | } 36 | } 37 | return 80 38 | } 39 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // Data model for Kingpin command-line structure. 10 | 11 | type FlagGroupModel struct { 12 | Flags []*FlagModel 13 | } 14 | 15 | func (f *FlagGroupModel) FlagSummary() string { 16 | out := []string{} 17 | count := 0 18 | for _, flag := range f.Flags { 19 | if flag.Name != "help" { 20 | count++ 21 | } 22 | if flag.Required { 23 | if flag.IsBoolFlag() { 24 | out = append(out, fmt.Sprintf("--[no-]%s", flag.Name)) 25 | } else { 26 | out = append(out, fmt.Sprintf("--%s=%s", flag.Name, flag.FormatPlaceHolder())) 27 | } 28 | } 29 | } 30 | if count != len(out) { 31 | out = append(out, "[]") 32 | } 33 | return strings.Join(out, " ") 34 | } 35 | 36 | type FlagModel struct { 37 | Name string 38 | Help string 39 | Short rune 40 | Default []string 41 | Envar string 42 | PlaceHolder string 43 | Required bool 44 | Hidden bool 45 | Value Value 46 | } 47 | 48 | func (f *FlagModel) String() string { 49 | return f.Value.String() 50 | } 51 | 52 | func (f *FlagModel) IsBoolFlag() bool { 53 | if fl, ok := f.Value.(boolFlag); ok { 54 | return fl.IsBoolFlag() 55 | } 56 | return false 57 | } 58 | 59 | func (f *FlagModel) FormatPlaceHolder() string { 60 | if f.PlaceHolder != "" { 61 | return f.PlaceHolder 62 | } 63 | if len(f.Default) > 0 { 64 | ellipsis := "" 65 | if len(f.Default) > 1 { 66 | ellipsis = "..." 67 | } 68 | if _, ok := f.Value.(*stringValue); ok { 69 | return strconv.Quote(f.Default[0]) + ellipsis 70 | } 71 | return f.Default[0] + ellipsis 72 | } 73 | return strings.ToUpper(f.Name) 74 | } 75 | 76 | type ArgGroupModel struct { 77 | Args []*ArgModel 78 | } 79 | 80 | func (a *ArgGroupModel) ArgSummary() string { 81 | depth := 0 82 | out := []string{} 83 | for _, arg := range a.Args { 84 | h := "<" + arg.Name + ">" 85 | if !arg.Required { 86 | h = "[" + h 87 | depth++ 88 | } 89 | out = append(out, h) 90 | } 91 | out[len(out)-1] = out[len(out)-1] + strings.Repeat("]", depth) 92 | return strings.Join(out, " ") 93 | } 94 | 95 | type ArgModel struct { 96 | Name string 97 | Help string 98 | Default []string 99 | Envar string 100 | Required bool 101 | Value Value 102 | } 103 | 104 | func (a *ArgModel) String() string { 105 | return a.Value.String() 106 | } 107 | 108 | type CmdGroupModel struct { 109 | Commands []*CmdModel 110 | } 111 | 112 | func (c *CmdGroupModel) FlattenedCommands() (out []*CmdModel) { 113 | for _, cmd := range c.Commands { 114 | if len(cmd.Commands) == 0 { 115 | out = append(out, cmd) 116 | } 117 | out = append(out, cmd.FlattenedCommands()...) 118 | } 119 | return 120 | } 121 | 122 | type CmdModel struct { 123 | Name string 124 | Aliases []string 125 | Help string 126 | FullCommand string 127 | Depth int 128 | Hidden bool 129 | Default bool 130 | Examples []Example 131 | *FlagGroupModel 132 | *ArgGroupModel 133 | *CmdGroupModel 134 | } 135 | 136 | func (c *CmdModel) String() string { 137 | return c.FullCommand 138 | } 139 | 140 | type ApplicationModel struct { 141 | Name string 142 | Help string 143 | Version string 144 | Author string 145 | Examples []Example 146 | *ArgGroupModel 147 | *CmdGroupModel 148 | *FlagGroupModel 149 | } 150 | 151 | func (a *Application) Model() *ApplicationModel { 152 | return &ApplicationModel{ 153 | Name: a.Name, 154 | Help: a.Help, 155 | Version: a.version, 156 | Author: a.author, 157 | FlagGroupModel: a.flagGroup.Model(), 158 | ArgGroupModel: a.argGroup.Model(), 159 | CmdGroupModel: a.cmdGroup.Model(), 160 | Examples: a.Examples(), 161 | } 162 | } 163 | 164 | func (a *argGroup) Model() *ArgGroupModel { 165 | m := &ArgGroupModel{} 166 | for _, arg := range a.args { 167 | m.Args = append(m.Args, arg.Model()) 168 | } 169 | return m 170 | } 171 | 172 | func (a *ArgClause) Model() *ArgModel { 173 | return &ArgModel{ 174 | Name: a.name, 175 | Help: a.help, 176 | Default: a.defaultValues, 177 | Envar: a.envar, 178 | Required: a.required, 179 | Value: a.value, 180 | } 181 | } 182 | 183 | func (f *flagGroup) Model() *FlagGroupModel { 184 | m := &FlagGroupModel{} 185 | for _, fl := range f.flagOrder { 186 | m.Flags = append(m.Flags, fl.Model()) 187 | } 188 | return m 189 | } 190 | 191 | func (f *FlagClause) Model() *FlagModel { 192 | return &FlagModel{ 193 | Name: f.name, 194 | Help: f.help, 195 | Short: rune(f.shorthand), 196 | Default: f.defaultValues, 197 | Envar: f.envar, 198 | PlaceHolder: f.placeholder, 199 | Required: f.required, 200 | Hidden: f.hidden, 201 | Value: f.value, 202 | } 203 | } 204 | 205 | func (c *cmdGroup) Model() *CmdGroupModel { 206 | m := &CmdGroupModel{} 207 | for _, cm := range c.commandOrder { 208 | m.Commands = append(m.Commands, cm.Model()) 209 | } 210 | return m 211 | } 212 | 213 | func (c *Cmd) Model() *CmdModel { 214 | depth := 0 215 | for i := c; i != nil; i = i.parent { 216 | depth++ 217 | } 218 | return &CmdModel{ 219 | Name: c.name, 220 | Aliases: c.aliases, 221 | Help: c.help, 222 | Depth: depth, 223 | Hidden: c.hidden, 224 | Default: c.isDefault, 225 | FullCommand: c.FullCommand(), 226 | FlagGroupModel: c.flagGroup.Model(), 227 | ArgGroupModel: c.argGroup.Model(), 228 | CmdGroupModel: c.cmdGroup.Model(), 229 | Examples: c.Examples(), 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "unicode/utf8" 9 | ) 10 | 11 | type TokenType int 12 | 13 | // Token types. 14 | const ( 15 | TokenShort TokenType = iota 16 | TokenLong 17 | TokenArg 18 | TokenError 19 | TokenEOL 20 | ) 21 | 22 | func (t TokenType) String() string { 23 | switch t { 24 | case TokenShort: 25 | return "short flag" 26 | case TokenLong: 27 | return "long flag" 28 | case TokenArg: 29 | return "argument" 30 | case TokenError: 31 | return "error" 32 | case TokenEOL: 33 | return "" 34 | } 35 | return "?" 36 | } 37 | 38 | var ( 39 | TokenEOLMarker = Token{-1, TokenEOL, ""} 40 | ) 41 | 42 | type Token struct { 43 | Index int 44 | Type TokenType 45 | Value string 46 | } 47 | 48 | func (t *Token) Equal(o *Token) bool { 49 | return t.Index == o.Index 50 | } 51 | 52 | func (t *Token) IsFlag() bool { 53 | return t.Type == TokenShort || t.Type == TokenLong 54 | } 55 | 56 | func (t *Token) IsEOF() bool { 57 | return t.Type == TokenEOL 58 | } 59 | 60 | func (t *Token) String() string { 61 | switch t.Type { 62 | case TokenShort: 63 | return "-" + t.Value 64 | case TokenLong: 65 | return "--" + t.Value 66 | case TokenArg: 67 | return t.Value 68 | case TokenError: 69 | return "error: " + t.Value 70 | case TokenEOL: 71 | return "" 72 | default: 73 | panic("unhandled type") 74 | } 75 | } 76 | 77 | // A union of possible elements in a parse stack. 78 | type ParseElement struct { 79 | // Clause is either *Cmd, *ArgClause or *FlagClause. 80 | Clause interface{} 81 | // Value is corresponding value for an ArgClause or FlagClause (if any). 82 | Value *string 83 | } 84 | 85 | // ParseContext holds the current context of the parser. When passed to 86 | // Action() callbacks Elements will be fully populated with *FlagClause, 87 | // *ArgClause and *Cmd values and their corresponding arguments (if 88 | // any). 89 | type ParseContext struct { 90 | SelectedCommand *Cmd 91 | ignoreDefault bool 92 | argsOnly bool 93 | peek []*Token 94 | argi int // Index of current command-line arg we're processing. 95 | args []string 96 | rawArgs []string 97 | flags *flagGroup 98 | arguments *argGroup 99 | argumenti int // Cursor into arguments 100 | // Flags, arguments and commands encountered and collected during parse. 101 | Elements []*ParseElement 102 | } 103 | 104 | func (p *ParseContext) nextArg() *ArgClause { 105 | if p.argumenti >= len(p.arguments.args) { 106 | return nil 107 | } 108 | arg := p.arguments.args[p.argumenti] 109 | if !arg.consumesRemainder() { 110 | p.argumenti++ 111 | } 112 | return arg 113 | } 114 | 115 | func (p *ParseContext) next() { 116 | p.argi++ 117 | p.args = p.args[1:] 118 | } 119 | 120 | // HasTrailingArgs returns true if there are unparsed command-line arguments. 121 | // This can occur if the parser can not match remaining arguments. 122 | func (p *ParseContext) HasTrailingArgs() bool { 123 | return len(p.args) > 0 124 | } 125 | 126 | func tokenize(args []string, ignoreDefault bool) *ParseContext { 127 | return &ParseContext{ 128 | ignoreDefault: ignoreDefault, 129 | args: args, 130 | rawArgs: args, 131 | flags: newFlagGroup(), 132 | arguments: newArgGroup(), 133 | } 134 | } 135 | 136 | func (p *ParseContext) mergeFlags(flags *flagGroup) { 137 | for _, flag := range flags.flagOrder { 138 | if flag.shorthand != 0 { 139 | p.flags.short[string(flag.shorthand)] = flag 140 | } 141 | p.flags.long[flag.name] = flag 142 | p.flags.flagOrder = append(p.flags.flagOrder, flag) 143 | } 144 | } 145 | 146 | func (p *ParseContext) mergeArgs(args *argGroup) { 147 | for _, arg := range args.args { 148 | p.arguments.args = append(p.arguments.args, arg) 149 | } 150 | } 151 | 152 | func (p *ParseContext) EOL() bool { 153 | return p.Peek().Type == TokenEOL 154 | } 155 | 156 | // Next token in the parse context. 157 | func (p *ParseContext) Next() *Token { 158 | if len(p.peek) > 0 { 159 | return p.pop() 160 | } 161 | 162 | // End of tokens. 163 | if len(p.args) == 0 { 164 | return &Token{Index: p.argi, Type: TokenEOL} 165 | } 166 | 167 | arg := p.args[0] 168 | p.next() 169 | 170 | if p.argsOnly { 171 | return &Token{p.argi, TokenArg, arg} 172 | } 173 | 174 | // All remaining args are passed directly. 175 | if arg == "--" { 176 | p.argsOnly = true 177 | return p.Next() 178 | } 179 | 180 | if strings.HasPrefix(arg, "--") { 181 | parts := strings.SplitN(arg[2:], "=", 2) 182 | token := &Token{p.argi, TokenLong, parts[0]} 183 | if len(parts) == 2 { 184 | p.Push(&Token{p.argi, TokenArg, parts[1]}) 185 | } 186 | return token 187 | } 188 | 189 | if strings.HasPrefix(arg, "-") { 190 | if len(arg) == 1 { 191 | return &Token{Index: p.argi, Type: TokenShort} 192 | } 193 | shortRune, size := utf8.DecodeRuneInString(arg[1:]) 194 | short := string(shortRune) 195 | flag, ok := p.flags.short[short] 196 | // Not a known short flag, we'll just return it anyway. 197 | if !ok { 198 | } else if fb, ok := flag.value.(boolFlag); ok && fb.IsBoolFlag() { 199 | // Bool short flag. 200 | } else { 201 | // Short flag with combined argument: -fARG 202 | token := &Token{p.argi, TokenShort, short} 203 | if len(arg) > size+1 { 204 | p.Push(&Token{p.argi, TokenArg, arg[size+1:]}) 205 | } 206 | return token 207 | } 208 | 209 | if len(arg) > size+1 { 210 | p.args = append([]string{"-" + arg[size+1:]}, p.args...) 211 | } 212 | return &Token{p.argi, TokenShort, short} 213 | } else if strings.HasPrefix(arg, "@") { 214 | expanded, err := ExpandArgsFromFile(arg[1:]) 215 | if err != nil { 216 | return &Token{p.argi, TokenError, err.Error()} 217 | } 218 | if len(p.args) == 0 { 219 | p.args = expanded 220 | } else { 221 | p.args = append(expanded, p.args...) 222 | } 223 | return p.Next() 224 | } 225 | 226 | return &Token{p.argi, TokenArg, arg} 227 | } 228 | 229 | func (p *ParseContext) Peek() *Token { 230 | if len(p.peek) == 0 { 231 | return p.Push(p.Next()) 232 | } 233 | return p.peek[len(p.peek)-1] 234 | } 235 | 236 | func (p *ParseContext) Push(token *Token) *Token { 237 | p.peek = append(p.peek, token) 238 | return token 239 | } 240 | 241 | func (p *ParseContext) pop() *Token { 242 | end := len(p.peek) - 1 243 | token := p.peek[end] 244 | p.peek = p.peek[0:end] 245 | return token 246 | } 247 | 248 | func (p *ParseContext) String() string { 249 | return p.SelectedCommand.FullCommand() 250 | } 251 | 252 | func (p *ParseContext) matchedFlag(flag *FlagClause, value string) { 253 | p.Elements = append(p.Elements, &ParseElement{Clause: flag, Value: &value}) 254 | } 255 | 256 | func (p *ParseContext) matchedArg(arg *ArgClause, value string) { 257 | p.Elements = append(p.Elements, &ParseElement{Clause: arg, Value: &value}) 258 | } 259 | 260 | func (p *ParseContext) matchedCmd(cmd *Cmd) { 261 | p.Elements = append(p.Elements, &ParseElement{Clause: cmd}) 262 | p.mergeFlags(cmd.flagGroup) 263 | p.mergeArgs(cmd.argGroup) 264 | p.SelectedCommand = cmd 265 | } 266 | 267 | // Expand arguments from a file. Lines starting with # will be treated as comments. 268 | func ExpandArgsFromFile(filename string) (out []string, err error) { 269 | r, err := os.Open(filename) 270 | if err != nil { 271 | return nil, err 272 | } 273 | defer r.Close() 274 | scanner := bufio.NewScanner(r) 275 | for scanner.Scan() { 276 | line := scanner.Text() 277 | if strings.HasPrefix(line, "#") { 278 | continue 279 | } 280 | out = append(out, line) 281 | } 282 | err = scanner.Err() 283 | return 284 | } 285 | 286 | func parse(context *ParseContext, app *Application) (err error) { 287 | context.mergeFlags(app.flagGroup) 288 | context.mergeArgs(app.argGroup) 289 | 290 | cmds := app.cmdGroup 291 | ignoreDefault := context.ignoreDefault 292 | 293 | loop: 294 | for !context.EOL() { 295 | token := context.Peek() 296 | 297 | switch token.Type { 298 | case TokenLong, TokenShort: 299 | if flag, err := context.flags.parse(context); err != nil { 300 | if !ignoreDefault { 301 | if cmd := cmds.defaultSubcommand(); cmd != nil { 302 | cmd.completionAlts = cmds.cmdNames() 303 | context.matchedCmd(cmd) 304 | cmds = cmd.cmdGroup 305 | break 306 | } 307 | } 308 | return err 309 | } else if flag == HelpFlag { 310 | ignoreDefault = true 311 | } 312 | 313 | case TokenArg: 314 | if cmds.have() { 315 | selectedDefault := false 316 | cmd, ok := cmds.commands[token.String()] 317 | if !ok { 318 | if !ignoreDefault { 319 | if cmd = cmds.defaultSubcommand(); cmd != nil { 320 | cmd.completionAlts = cmds.cmdNames() 321 | selectedDefault = true 322 | } 323 | } 324 | if cmd == nil { 325 | return fmt.Errorf("expected command but got %q", token) 326 | } 327 | } 328 | if cmd == HelpCommand { 329 | ignoreDefault = true 330 | } 331 | cmd.completionAlts = nil 332 | context.matchedCmd(cmd) 333 | cmds = cmd.cmdGroup 334 | if !selectedDefault { 335 | context.Next() 336 | } 337 | } else if context.arguments.have() { 338 | if app.noInterspersed { 339 | // no more flags 340 | context.argsOnly = true 341 | } 342 | arg := context.nextArg() 343 | if arg == nil { 344 | break loop 345 | } 346 | context.matchedArg(arg, token.String()) 347 | context.Next() 348 | } else { 349 | break loop 350 | } 351 | 352 | case TokenEOL: 353 | break loop 354 | } 355 | } 356 | 357 | // Move to innermost default command. 358 | for !ignoreDefault { 359 | if cmd := cmds.defaultSubcommand(); cmd != nil { 360 | cmd.completionAlts = cmds.cmdNames() 361 | context.matchedCmd(cmd) 362 | cmds = cmd.cmdGroup 363 | } else { 364 | break 365 | } 366 | } 367 | 368 | if !context.EOL() { 369 | return fmt.Errorf("unexpected %s", context.Peek()) 370 | } 371 | 372 | // Set defaults for all remaining args. 373 | for arg := context.nextArg(); arg != nil && !arg.consumesRemainder(); arg = context.nextArg() { 374 | for _, defaultValue := range arg.defaultValues { 375 | if err := arg.value.Set(defaultValue); err != nil { 376 | return fmt.Errorf("invalid default value '%s' for argument '%s'", defaultValue, arg.name) 377 | } 378 | } 379 | } 380 | 381 | return 382 | } 383 | -------------------------------------------------------------------------------- /parser_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert" 9 | ) 10 | 11 | func TestParserExpandFromFile(t *testing.T) { 12 | f, err := ioutil.TempFile("", "") 13 | assert.NoError(t, err) 14 | defer os.Remove(f.Name()) 15 | f.WriteString("hello\nworld\n") 16 | f.Close() 17 | 18 | app := New("test", "") 19 | arg0 := app.Arg("arg0", "").String() 20 | arg1 := app.Arg("arg1", "").String() 21 | 22 | _, err = app.Parse([]string{"@" + f.Name()}) 23 | assert.NoError(t, err) 24 | assert.Equal(t, "hello", *arg0) 25 | assert.Equal(t, "world", *arg1) 26 | } 27 | 28 | func TestParserExpandFromFileLeadingArg(t *testing.T) { 29 | f, err := ioutil.TempFile("", "") 30 | assert.NoError(t, err) 31 | defer os.Remove(f.Name()) 32 | f.WriteString("hello\nworld\n") 33 | f.Close() 34 | 35 | app := New("test", "") 36 | arg0 := app.Arg("arg0", "").String() 37 | arg1 := app.Arg("arg1", "").String() 38 | arg2 := app.Arg("arg2", "").String() 39 | 40 | _, err = app.Parse([]string{"prefix", "@" + f.Name()}) 41 | assert.NoError(t, err) 42 | assert.Equal(t, "prefix", *arg0) 43 | assert.Equal(t, "hello", *arg1) 44 | assert.Equal(t, "world", *arg2) 45 | } 46 | 47 | func TestParserExpandFromFileTrailingArg(t *testing.T) { 48 | f, err := ioutil.TempFile("", "") 49 | assert.NoError(t, err) 50 | defer os.Remove(f.Name()) 51 | f.WriteString("hello\nworld\n") 52 | f.Close() 53 | 54 | app := New("test", "") 55 | arg0 := app.Arg("arg0", "").String() 56 | arg1 := app.Arg("arg1", "").String() 57 | arg2 := app.Arg("arg2", "").String() 58 | 59 | _, err = app.Parse([]string{"@" + f.Name(), "suffix"}) 60 | assert.NoError(t, err) 61 | assert.Equal(t, "hello", *arg0) 62 | assert.Equal(t, "world", *arg1) 63 | assert.Equal(t, "suffix", *arg2) 64 | } 65 | 66 | func TestParserExpandFromFileMultipleSurroundingArgs(t *testing.T) { 67 | f, err := ioutil.TempFile("", "") 68 | assert.NoError(t, err) 69 | defer os.Remove(f.Name()) 70 | f.WriteString("hello\nworld\n") 71 | f.Close() 72 | 73 | app := New("test", "") 74 | arg0 := app.Arg("arg0", "").String() 75 | arg1 := app.Arg("arg1", "").String() 76 | arg2 := app.Arg("arg2", "").String() 77 | arg3 := app.Arg("arg3", "").String() 78 | 79 | _, err = app.Parse([]string{"prefix", "@" + f.Name(), "suffix"}) 80 | assert.NoError(t, err) 81 | assert.Equal(t, "prefix", *arg0) 82 | assert.Equal(t, "hello", *arg1) 83 | assert.Equal(t, "world", *arg2) 84 | assert.Equal(t, "suffix", *arg3) 85 | } 86 | 87 | func TestParserExpandFromFileMultipleFlags(t *testing.T) { 88 | f, err := ioutil.TempFile("", "") 89 | assert.NoError(t, err) 90 | defer os.Remove(f.Name()) 91 | f.WriteString("--flag1=f1\n--flag2=f2\n") 92 | f.Close() 93 | 94 | app := New("test", "") 95 | flag0 := app.Flag("flag0", "").String() 96 | flag1 := app.Flag("flag1", "").String() 97 | flag2 := app.Flag("flag2", "").String() 98 | flag3 := app.Flag("flag3", "").String() 99 | 100 | _, err = app.Parse([]string{"--flag0=f0", "@" + f.Name(), "--flag3=f3"}) 101 | assert.NoError(t, err) 102 | assert.Equal(t, "f0", *flag0) 103 | assert.Equal(t, "f1", *flag1) 104 | assert.Equal(t, "f2", *flag2) 105 | assert.Equal(t, "f3", *flag3) 106 | } 107 | 108 | func TestParseContextPush(t *testing.T) { 109 | app := New("test", "") 110 | app.Command("foo", "").Command("bar", "") 111 | c := tokenize([]string{"foo", "bar"}, false) 112 | a := c.Next() 113 | assert.Equal(t, TokenArg, a.Type) 114 | b := c.Next() 115 | assert.Equal(t, TokenArg, b.Type) 116 | c.Push(b) 117 | c.Push(a) 118 | a = c.Next() 119 | assert.Equal(t, "foo", a.Value) 120 | b = c.Next() 121 | assert.Equal(t, "bar", b.Value) 122 | } 123 | -------------------------------------------------------------------------------- /parsers.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | "os" 7 | "time" 8 | 9 | "github.com/alecthomas/units" 10 | ) 11 | 12 | type Settings interface { 13 | SetValue(value Value) 14 | } 15 | 16 | type parserMixin struct { 17 | value Value 18 | required bool 19 | } 20 | 21 | func (p *parserMixin) SetValue(value Value) { 22 | p.value = value 23 | } 24 | 25 | // StringMap provides key=value parsing into a map. 26 | func (p *parserMixin) StringMap() (target *map[string]string) { 27 | target = &(map[string]string{}) 28 | p.StringMapVar(target) 29 | return 30 | } 31 | 32 | // Duration sets the parser to a time.Duration parser. 33 | func (p *parserMixin) Duration() (target *time.Duration) { 34 | target = new(time.Duration) 35 | p.DurationVar(target) 36 | return 37 | } 38 | 39 | // Bytes parses numeric byte units. eg. 1.5KB 40 | func (p *parserMixin) Bytes() (target *units.Base2Bytes) { 41 | target = new(units.Base2Bytes) 42 | p.BytesVar(target) 43 | return 44 | } 45 | 46 | // IP sets the parser to a net.IP parser. 47 | func (p *parserMixin) IP() (target *net.IP) { 48 | target = new(net.IP) 49 | p.IPVar(target) 50 | return 51 | } 52 | 53 | // TCP (host:port) address. 54 | func (p *parserMixin) TCP() (target **net.TCPAddr) { 55 | target = new(*net.TCPAddr) 56 | p.TCPVar(target) 57 | return 58 | } 59 | 60 | // TCPVar (host:port) address. 61 | func (p *parserMixin) TCPVar(target **net.TCPAddr) { 62 | p.SetValue(newTCPAddrValue(target)) 63 | } 64 | 65 | // ExistingFile sets the parser to one that requires and returns an existing file. 66 | func (p *parserMixin) ExistingFile() (target *string) { 67 | target = new(string) 68 | p.ExistingFileVar(target) 69 | return 70 | } 71 | 72 | // ExistingDir sets the parser to one that requires and returns an existing directory. 73 | func (p *parserMixin) ExistingDir() (target *string) { 74 | target = new(string) 75 | p.ExistingDirVar(target) 76 | return 77 | } 78 | 79 | // ExistingFileOrDir sets the parser to one that requires and returns an existing file OR directory. 80 | func (p *parserMixin) ExistingFileOrDir() (target *string) { 81 | target = new(string) 82 | p.ExistingFileOrDirVar(target) 83 | return 84 | } 85 | 86 | // File returns an os.File against an existing file. 87 | func (p *parserMixin) File() (target **os.File) { 88 | target = new(*os.File) 89 | p.FileVar(target) 90 | return 91 | } 92 | 93 | // File attempts to open a File with os.OpenFile(flag, perm). 94 | func (p *parserMixin) OpenFile(flag int, perm os.FileMode) (target **os.File) { 95 | target = new(*os.File) 96 | p.OpenFileVar(target, flag, perm) 97 | return 98 | } 99 | 100 | // URL provides a valid, parsed url.URL. 101 | func (p *parserMixin) URL() (target **url.URL) { 102 | target = new(*url.URL) 103 | p.URLVar(target) 104 | return 105 | } 106 | 107 | // StringMap provides key=value parsing into a map. 108 | func (p *parserMixin) StringMapVar(target *map[string]string) { 109 | p.SetValue(newStringMapValue(target)) 110 | } 111 | 112 | // Float sets the parser to a float64 parser. 113 | func (p *parserMixin) Float() (target *float64) { 114 | return p.Float64() 115 | } 116 | 117 | // Float sets the parser to a float64 parser. 118 | func (p *parserMixin) FloatVar(target *float64) { 119 | p.Float64Var(target) 120 | } 121 | 122 | // Duration sets the parser to a time.Duration parser. 123 | func (p *parserMixin) DurationVar(target *time.Duration) { 124 | p.SetValue(newDurationValue(target)) 125 | } 126 | 127 | // BytesVar parses numeric byte units. eg. 1.5KB 128 | func (p *parserMixin) BytesVar(target *units.Base2Bytes) { 129 | p.SetValue(newBytesValue(target)) 130 | } 131 | 132 | // IP sets the parser to a net.IP parser. 133 | func (p *parserMixin) IPVar(target *net.IP) { 134 | p.SetValue(newIPValue(target)) 135 | } 136 | 137 | // ExistingFile sets the parser to one that requires and returns an existing file. 138 | func (p *parserMixin) ExistingFileVar(target *string) { 139 | p.SetValue(newExistingFileValue(target)) 140 | } 141 | 142 | // ExistingDir sets the parser to one that requires and returns an existing directory. 143 | func (p *parserMixin) ExistingDirVar(target *string) { 144 | p.SetValue(newExistingDirValue(target)) 145 | } 146 | 147 | // ExistingDir sets the parser to one that requires and returns an existing directory. 148 | func (p *parserMixin) ExistingFileOrDirVar(target *string) { 149 | p.SetValue(newExistingFileOrDirValue(target)) 150 | } 151 | 152 | // FileVar opens an existing file. 153 | func (p *parserMixin) FileVar(target **os.File) { 154 | p.SetValue(newFileValue(target, os.O_RDONLY, 0)) 155 | } 156 | 157 | // OpenFileVar calls os.OpenFile(flag, perm) 158 | func (p *parserMixin) OpenFileVar(target **os.File, flag int, perm os.FileMode) { 159 | p.SetValue(newFileValue(target, flag, perm)) 160 | } 161 | 162 | // URL provides a valid, parsed url.URL. 163 | func (p *parserMixin) URLVar(target **url.URL) { 164 | p.SetValue(newURLValue(target)) 165 | } 166 | 167 | // URLList provides a parsed list of url.URL values. 168 | func (p *parserMixin) URLList() (target *[]*url.URL) { 169 | target = new([]*url.URL) 170 | p.URLListVar(target) 171 | return 172 | } 173 | 174 | // URLListVar provides a parsed list of url.URL values. 175 | func (p *parserMixin) URLListVar(target *[]*url.URL) { 176 | p.SetValue(newURLListValue(target)) 177 | } 178 | 179 | // Enum allows a value from a set of options. 180 | func (p *parserMixin) Enum(options ...string) (target *string) { 181 | target = new(string) 182 | p.EnumVar(target, options...) 183 | return 184 | } 185 | 186 | // EnumVar allows a value from a set of options. 187 | func (p *parserMixin) EnumVar(target *string, options ...string) { 188 | p.SetValue(newEnumFlag(target, options...)) 189 | } 190 | 191 | // Enums allows a set of values from a set of options. 192 | func (p *parserMixin) Enums(options ...string) (target *[]string) { 193 | target = new([]string) 194 | p.EnumsVar(target, options...) 195 | return 196 | } 197 | 198 | // EnumVar allows a value from a set of options. 199 | func (p *parserMixin) EnumsVar(target *[]string, options ...string) { 200 | p.SetValue(newEnumsFlag(target, options...)) 201 | } 202 | 203 | // A Counter increments a number each time it is encountered. 204 | func (p *parserMixin) Counter() (target *int) { 205 | target = new(int) 206 | p.CounterVar(target) 207 | return 208 | } 209 | 210 | func (p *parserMixin) CounterVar(target *int) { 211 | p.SetValue(newCounterValue(target)) 212 | } 213 | -------------------------------------------------------------------------------- /parsers_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "io/ioutil" 5 | "net" 6 | "net/url" 7 | "os" 8 | 9 | "github.com/alecthomas/assert" 10 | 11 | "testing" 12 | ) 13 | 14 | func TestParseStrings(t *testing.T) { 15 | p := parserMixin{} 16 | v := p.Strings() 17 | p.value.Set("a") 18 | p.value.Set("b") 19 | assert.Equal(t, []string{"a", "b"}, *v) 20 | } 21 | 22 | func TestStringsStringer(t *testing.T) { 23 | target := []string{} 24 | v := newAccumulator(&target, func(v interface{}) Value { return newStringValue(v.(*string)) }) 25 | v.Set("hello") 26 | v.Set("world") 27 | assert.Equal(t, "hello,world", v.String()) 28 | } 29 | 30 | func TestParseStringMap(t *testing.T) { 31 | p := parserMixin{} 32 | v := p.StringMap() 33 | p.value.Set("a:b") 34 | p.value.Set("b:c") 35 | assert.Equal(t, map[string]string{"a": "b", "b": "c"}, *v) 36 | } 37 | 38 | func TestParseIP(t *testing.T) { 39 | p := parserMixin{} 40 | v := p.IP() 41 | p.value.Set("10.1.1.2") 42 | ip := net.ParseIP("10.1.1.2") 43 | assert.Equal(t, ip, *v) 44 | } 45 | 46 | func TestParseURL(t *testing.T) { 47 | p := parserMixin{} 48 | v := p.URL() 49 | p.value.Set("http://w3.org") 50 | u, err := url.Parse("http://w3.org") 51 | assert.NoError(t, err) 52 | assert.Equal(t, *u, **v) 53 | } 54 | 55 | func TestParseExistingFile(t *testing.T) { 56 | f, err := ioutil.TempFile("", "") 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | defer f.Close() 61 | defer os.Remove(f.Name()) 62 | 63 | p := parserMixin{} 64 | v := p.ExistingFile() 65 | err = p.value.Set(f.Name()) 66 | assert.NoError(t, err) 67 | assert.Equal(t, f.Name(), *v) 68 | err = p.value.Set("/etc/hostsDEFINITELYMISSING") 69 | assert.Error(t, err) 70 | } 71 | 72 | func TestParseTCPAddr(t *testing.T) { 73 | p := parserMixin{} 74 | v := p.TCP() 75 | err := p.value.Set("127.0.0.1:1234") 76 | assert.NoError(t, err) 77 | expected, err := net.ResolveTCPAddr("tcp", "127.0.0.1:1234") 78 | assert.NoError(t, err) 79 | assert.Equal(t, *expected, **v) 80 | } 81 | 82 | func TestParseTCPAddrList(t *testing.T) { 83 | p := parserMixin{} 84 | _ = p.TCPList() 85 | err := p.value.Set("127.0.0.1:1234") 86 | assert.NoError(t, err) 87 | err = p.value.Set("127.0.0.1:1235") 88 | assert.NoError(t, err) 89 | assert.Equal(t, "127.0.0.1:1234,127.0.0.1:1235", p.value.String()) 90 | } 91 | 92 | func TestFloat32(t *testing.T) { 93 | p := parserMixin{} 94 | v := p.Float32() 95 | err := p.value.Set("123.45") 96 | assert.NoError(t, err) 97 | assert.InEpsilon(t, 123.45, *v, 0.001) 98 | } 99 | -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | // Default usage template. 4 | var DefaultUsageTemplate = `{{define "FormatCommand"}}\ 5 | {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ 6 | {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 7 | {{end}}\ 8 | 9 | {{define "FormatCommands"}}\ 10 | {{range .FlattenedCommands}}\ 11 | {{if not .Hidden}}\ 12 | {{printf "%-20s %s" .FullCommand .Help}} 13 | {{end}}\ 14 | {{end}}\ 15 | {{end}}\ 16 | 17 | {{define "FormatUsage"}}\ 18 | {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}} 19 | {{end}} 20 | {{if .Context.SelectedCommand}}\ 21 | {{.Context.SelectedCommand.Help | Wrap 2}} 22 | {{"Usage:" | bold}} 23 | 24 | {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}} 25 | {{else}}\ 26 | {{"Usage:" | bold}} 27 | 28 | {{.App.Name}}{{template "FormatUsage" .App}} 29 | {{end}}\ 30 | {{if .Context.Flags}}\ 31 | {{"Flags:" | bold}} 32 | 33 | {{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}} 34 | {{end}}\ 35 | {{if .Context.Args}}\ 36 | {{"Args:" | bold}} 37 | 38 | {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}} 39 | {{end}}\ 40 | {{if .Context.SelectedCommand}}\ 41 | {{if len .Context.SelectedCommand.Commands}}\ 42 | {{"Subcommands:" | bold}} 43 | 44 | {{template "FormatCommands" .Context.SelectedCommand}} 45 | {{end}}\ 46 | {{else if .App.Commands}}\ 47 | {{"Commands:" | bold}} 48 | 49 | {{template "FormatCommands" .App}} 50 | {{end}}\ 51 | {{define "Examples"}}\ 52 | {{if .}}\ 53 | {{"Examples:" | bold}} 54 | {{range .}} 55 | {{.Help}} 56 | $ {{.Usage}} 57 | {{end}} 58 | {{end}}\ 59 | {{end}}\ 60 | {{if .Context.SelectedCommand}}\ 61 | {{template "Examples" .Context.SelectedCommand.Examples}}\ 62 | {{else if .App.Examples}}\ 63 | {{template "Examples" .App.Examples}}\ 64 | {{end}}\ 65 | ` 66 | 67 | // Usage template where command's optional flags are listed separately 68 | var SeparateOptionalFlagsUsageTemplate = `{{define "FormatCommand"}}\ 69 | {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ 70 | {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 71 | {{end}}\ 72 | 73 | {{define "FormatCommands"}}\ 74 | {{range .FlattenedCommands}}\ 75 | {{if not .Hidden}}\ 76 | {{.FullCommand}}{{if .Default}}*{{end}}{{template "FormatCommand" .}} 77 | {{.Help|Wrap 4}} 78 | {{end}}\ 79 | {{end}}\ 80 | {{end}}\ 81 | 82 | {{define "FormatUsage"}}\ 83 | {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}} 84 | {{if .Help}} 85 | {{.Help|Wrap 0}}\ 86 | {{end}}\ 87 | 88 | {{end}}\ 89 | {{if .Context.SelectedCommand}}\ 90 | usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}} 91 | {{else}}\ 92 | usage: {{.App.Name}}{{template "FormatUsage" .App}} 93 | {{end}}\ 94 | 95 | {{if .Context.Flags|RequiredFlags}}\ 96 | Required flags: 97 | {{.Context.Flags|RequiredFlags|FlagsToTwoColumns|FormatTwoColumns}} 98 | {{end}}\ 99 | {{if .Context.Flags|OptionalFlags}}\ 100 | Optional flags: 101 | {{.Context.Flags|OptionalFlags|FlagsToTwoColumns|FormatTwoColumns}} 102 | {{end}}\ 103 | {{if .Context.Args}}\ 104 | Args: 105 | {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}} 106 | {{end}}\ 107 | {{if .Context.SelectedCommand}}\ 108 | Subcommands: 109 | {{if .Context.SelectedCommand.Commands}}\ 110 | {{template "FormatCommands" .Context.SelectedCommand}} 111 | {{end}}\ 112 | {{else if .App.Commands}}\ 113 | Commands: 114 | {{template "FormatCommands" .App}} 115 | {{end}}\ 116 | ` 117 | 118 | // Usage template with compactly formatted commands. 119 | var CompactUsageTemplate = `{{define "FormatCommand"}}\ 120 | {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ 121 | {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 122 | {{end}}\ 123 | 124 | {{define "FormatCommandList"}}\ 125 | {{range .}}\ 126 | {{if not .Hidden}}\ 127 | {{.Depth|Indent}}{{.Name}}{{if .Default}}*{{end}}{{template "FormatCommand" .}} 128 | {{end}}\ 129 | {{template "FormatCommandList" .Commands}}\ 130 | {{end}}\ 131 | {{end}}\ 132 | 133 | {{define "FormatUsage"}}\ 134 | {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}} 135 | {{if .Help}} 136 | {{.Help|Wrap 0}}\ 137 | {{end}}\ 138 | 139 | {{end}}\ 140 | 141 | {{if .Context.SelectedCommand}}\ 142 | usage: {{.App.Name}} {{.Context.SelectedCommand}}{{template "FormatUsage" .Context.SelectedCommand}} 143 | {{else}}\ 144 | usage: {{.App.Name}}{{template "FormatUsage" .App}} 145 | {{end}}\ 146 | {{if .Context.Flags}}\ 147 | Flags: 148 | {{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}} 149 | {{end}}\ 150 | {{if .Context.Args}}\ 151 | Args: 152 | {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}} 153 | {{end}}\ 154 | {{if .Context.SelectedCommand}}\ 155 | {{if .Context.SelectedCommand.Commands}}\ 156 | Commands: 157 | {{.Context.SelectedCommand}} 158 | {{template "FormatCommandList" .Context.SelectedCommand.Commands}} 159 | {{end}}\ 160 | {{else if .App.Commands}}\ 161 | Commands: 162 | {{template "FormatCommandList" .App.Commands}} 163 | {{end}}\ 164 | ` 165 | 166 | var ManPageTemplate = `{{define "FormatFlags"}}\ 167 | {{range .Flags}}\ 168 | {{if not .Hidden}}\ 169 | .TP 170 | \fB{{if .Short}}-{{.Short|Char}}, {{end}}--{{.Name}}{{if not .IsBoolFlag}}={{.FormatPlaceHolder}}{{end}}\\fR 171 | {{.Help}} 172 | {{end}}\ 173 | {{end}}\ 174 | {{end}}\ 175 | 176 | {{define "FormatCommand"}}\ 177 | {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ 178 | {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}{{if .Default}}*{{end}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 179 | {{end}}\ 180 | 181 | {{define "FormatCommands"}}\ 182 | {{range .FlattenedCommands}}\ 183 | {{if not .Hidden}}\ 184 | .SS 185 | \fB{{.FullCommand}}{{template "FormatCommand" .}}\\fR 186 | .PP 187 | {{.Help}} 188 | {{template "FormatFlags" .}}\ 189 | {{end}}\ 190 | {{end}}\ 191 | {{end}}\ 192 | 193 | {{define "FormatUsage"}}\ 194 | {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}}\\fR 195 | {{end}}\ 196 | 197 | .TH {{.App.Name}} 1 {{.App.Version}} "{{.App.Author}}" 198 | .SH "NAME" 199 | {{.App.Name}} 200 | .SH "SYNOPSIS" 201 | .TP 202 | \fB{{.App.Name}}{{template "FormatUsage" .App}} 203 | .SH "DESCRIPTION" 204 | {{.App.Help}} 205 | .SH "OPTIONS" 206 | {{template "FormatFlags" .App}}\ 207 | {{if .App.Commands}}\ 208 | .SH "COMMANDS" 209 | {{template "FormatCommands" .App}}\ 210 | {{end}}\ 211 | ` 212 | 213 | // Default usage template. 214 | var LongHelpTemplate = `{{define "FormatCommand"}}\ 215 | {{if .FlagSummary}} {{.FlagSummary}}{{end}}\ 216 | {{range .Args}} {{if not .Required}}[{{end}}<{{.Name}}>{{if .Value|IsCumulative}}...{{end}}{{if not .Required}}]{{end}}{{end}}\ 217 | {{end}}\ 218 | 219 | {{define "FormatCommands"}}\ 220 | {{range .FlattenedCommands}}\ 221 | {{if not .Hidden}}\ 222 | {{.FullCommand}}{{template "FormatCommand" .}} 223 | {{.Help|Wrap 4}} 224 | {{with .Flags|FlagsToTwoColumns}}{{FormatTwoColumnsWithIndent . 4 2}}{{end}} 225 | {{end}}\ 226 | {{end}}\ 227 | {{end}}\ 228 | 229 | {{define "FormatUsage"}}\ 230 | {{template "FormatCommand" .}}{{if .Commands}} [ ...]{{end}} 231 | {{if .Help}} 232 | {{.Help|Wrap 0}}\ 233 | {{end}}\ 234 | 235 | {{end}}\ 236 | 237 | usage: {{.App.Name}}{{template "FormatUsage" .App}} 238 | {{if .Context.Flags}}\ 239 | Flags: 240 | {{.Context.Flags|FlagsToTwoColumns|FormatTwoColumns}} 241 | {{end}}\ 242 | {{if .Context.Args}}\ 243 | Args: 244 | {{.Context.Args|ArgsToTwoColumns|FormatTwoColumns}} 245 | {{end}}\ 246 | {{if .App.Commands}}\ 247 | Commands: 248 | {{template "FormatCommands" .App}} 249 | {{end}}\ 250 | ` 251 | 252 | var BashCompletionTemplate = ` 253 | _{{.App.Name}}_bash_autocomplete() { 254 | local cur prev opts base 255 | COMPREPLY=() 256 | cur="${COMP_WORDS[COMP_CWORD]}" 257 | opts=$( ${COMP_WORDS[0]} --completion-bash ${COMP_WORDS[@]:1:$COMP_CWORD} ) 258 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 259 | return 0 260 | } 261 | complete -F _{{.App.Name}}_bash_autocomplete {{.App.Name}} 262 | 263 | ` 264 | 265 | var ZshCompletionTemplate = ` 266 | #compdef {{.App.Name}} 267 | autoload -U compinit && compinit 268 | autoload -U bashcompinit && bashcompinit 269 | 270 | _{{.App.Name}}_bash_autocomplete() { 271 | local cur prev opts base 272 | COMPREPLY=() 273 | cur="${COMP_WORDS[COMP_CWORD]}" 274 | opts=$( ${COMP_WORDS[0]} --completion-bash ${COMP_WORDS[@]:1:$COMP_CWORD} ) 275 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 276 | return 0 277 | } 278 | complete -F _{{.App.Name}}_bash_autocomplete {{.App.Name}} 279 | ` 280 | -------------------------------------------------------------------------------- /usage.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/doc" 7 | "io" 8 | "strings" 9 | 10 | "github.com/alecthomas/template" 11 | ) 12 | 13 | var ( 14 | preIndent = " " 15 | ) 16 | 17 | func formatTwoColumns(w io.Writer, indent, padding, width int, rows [][2]string) { 18 | // Find size of first column. 19 | s := 0 20 | for _, row := range rows { 21 | if c := len(row[0]); c > s && c < 30 { 22 | s = c 23 | } 24 | } 25 | 26 | indentStr := strings.Repeat(" ", indent) 27 | offsetStr := strings.Repeat(" ", s+padding) 28 | 29 | for _, row := range rows { 30 | buf := bytes.NewBuffer(nil) 31 | doc.ToText(buf, row[1], "", preIndent, width-s-padding-indent) 32 | lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") 33 | fmt.Fprintf(w, "%s%-*s%*s", indentStr, s, row[0], padding, "") 34 | if len(row[0]) >= 30 { 35 | fmt.Fprintf(w, "\n%s%s", indentStr, offsetStr) 36 | } 37 | fmt.Fprintf(w, "%s\n", lines[0]) 38 | for _, line := range lines[1:] { 39 | fmt.Fprintf(w, "%s%s%s\n", indentStr, offsetStr, line) 40 | } 41 | } 42 | } 43 | 44 | // Usage writes application usage to w. It parses args to determine 45 | // appropriate help context, such as which command to show help for. 46 | func (a *Application) Usage(args []string) { 47 | context, err := a.parseContext(true, args) 48 | a.FatalIfError(err, "") 49 | if err := a.UsageForContextWithTemplate(context, 2, a.usageTemplate); err != nil { 50 | panic(err) 51 | } 52 | } 53 | 54 | func formatAppUsage(app *ApplicationModel) string { 55 | s := []string{app.Name} 56 | if len(app.Flags) > 0 { 57 | s = append(s, app.FlagSummary()) 58 | } 59 | if len(app.Args) > 0 { 60 | s = append(s, app.ArgSummary()) 61 | } 62 | return strings.Join(s, " ") 63 | } 64 | 65 | func formatCmdUsage(app *ApplicationModel, cmd *CmdModel) string { 66 | s := []string{app.Name, cmd.String()} 67 | if len(app.Flags) > 0 { 68 | s = append(s, app.FlagSummary()) 69 | } 70 | if len(app.Args) > 0 { 71 | s = append(s, app.ArgSummary()) 72 | } 73 | return strings.Join(s, " ") 74 | } 75 | 76 | func formatFlag(haveShort bool, flag *FlagModel) string { 77 | flagString := " " 78 | if flag.Short != 0 { 79 | flagString += fmt.Sprintf("-%c, --%s", flag.Short, flag.Name) 80 | } else { 81 | if haveShort { 82 | flagString += fmt.Sprintf(" --%s", flag.Name) 83 | } else { 84 | flagString += fmt.Sprintf("--%s", flag.Name) 85 | } 86 | } 87 | if !flag.IsBoolFlag() { 88 | flagString += fmt.Sprintf("=%s", flag.FormatPlaceHolder()) 89 | } 90 | if v, ok := flag.Value.(repeatableFlag); ok && v.IsCumulative() { 91 | flagString += " ..." 92 | } 93 | return flagString 94 | } 95 | 96 | type templateParseContext struct { 97 | SelectedCommand *CmdModel 98 | *FlagGroupModel 99 | *ArgGroupModel 100 | } 101 | 102 | type templateContext struct { 103 | App *ApplicationModel 104 | Width int 105 | Context *templateParseContext 106 | } 107 | 108 | // UsageForContext displays usage information from a ParseContext (obtained from 109 | // Application.ParseContext() or Action(f) callbacks). 110 | func (a *Application) UsageForContext(context *ParseContext) error { 111 | return a.UsageForContextWithTemplate(context, 2, a.usageTemplate) 112 | } 113 | 114 | // UsageForContextWithTemplate is the base usage function. You generally don't need to use this. 115 | func (a *Application) UsageForContextWithTemplate(context *ParseContext, indent int, tmpl string) error { 116 | width := guessWidth(a.usageWriter) 117 | funcs := template.FuncMap{ 118 | "Indent": func(level int) string { 119 | return strings.Repeat(" ", level*indent) 120 | }, 121 | "Wrap": func(indent int, s string) string { 122 | buf := bytes.NewBuffer(nil) 123 | indentText := strings.Repeat(" ", indent) 124 | doc.ToText(buf, s, indentText, " "+indentText, width-indent) 125 | return buf.String() 126 | }, 127 | "FormatFlag": formatFlag, 128 | "FlagsToTwoColumns": func(f []*FlagModel) [][2]string { 129 | rows := [][2]string{} 130 | haveShort := false 131 | for _, flag := range f { 132 | if flag.Short != 0 { 133 | haveShort = true 134 | break 135 | } 136 | } 137 | for _, flag := range f { 138 | if !flag.Hidden { 139 | rows = append(rows, [2]string{formatFlag(haveShort, flag), flag.Help}) 140 | } 141 | } 142 | return rows 143 | }, 144 | "RequiredFlags": func(f []*FlagModel) []*FlagModel { 145 | requiredFlags := []*FlagModel{} 146 | for _, flag := range f { 147 | if flag.Required { 148 | requiredFlags = append(requiredFlags, flag) 149 | } 150 | } 151 | return requiredFlags 152 | }, 153 | "OptionalFlags": func(f []*FlagModel) []*FlagModel { 154 | optionalFlags := []*FlagModel{} 155 | for _, flag := range f { 156 | if !flag.Required { 157 | optionalFlags = append(optionalFlags, flag) 158 | } 159 | } 160 | return optionalFlags 161 | }, 162 | "ArgsToTwoColumns": func(a []*ArgModel) [][2]string { 163 | rows := [][2]string{} 164 | for _, arg := range a { 165 | s := "<" + arg.Name + ">" 166 | if !arg.Required { 167 | s = "[" + s + "]" 168 | } 169 | rows = append(rows, [2]string{" " + s, arg.Help}) 170 | } 171 | return rows 172 | }, 173 | "FormatTwoColumns": func(rows [][2]string) string { 174 | buf := bytes.NewBuffer(nil) 175 | formatTwoColumns(buf, indent, indent, width, rows) 176 | return buf.String() 177 | }, 178 | "FormatTwoColumnsWithIndent": func(rows [][2]string, indent, padding int) string { 179 | buf := bytes.NewBuffer(nil) 180 | formatTwoColumns(buf, indent, padding, width, rows) 181 | return buf.String() 182 | }, 183 | "FormatAppUsage": formatAppUsage, 184 | "FormatCommandUsage": formatCmdUsage, 185 | "IsCumulative": func(value Value) bool { 186 | r, ok := value.(remainderArg) 187 | return ok && r.IsCumulative() 188 | }, 189 | "Char": func(c rune) string { 190 | return string(c) 191 | }, 192 | "bold": func(s string) string { 193 | return fmt.Sprintf("\033[1m%s\033[0m", s) 194 | }, 195 | } 196 | t, err := template.New("usage").Funcs(funcs).Parse(tmpl) 197 | if err != nil { 198 | return err 199 | } 200 | var selectedCommand *CmdModel 201 | if context.SelectedCommand != nil { 202 | selectedCommand = context.SelectedCommand.Model() 203 | } 204 | ctx := templateContext{ 205 | App: a.Model(), 206 | Width: width, 207 | Context: &templateParseContext{ 208 | SelectedCommand: selectedCommand, 209 | FlagGroupModel: context.flags.Model(), 210 | ArgGroupModel: context.arguments.Model(), 211 | }, 212 | } 213 | return t.Execute(a.usageWriter, ctx) 214 | } 215 | -------------------------------------------------------------------------------- /usage_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert" 9 | ) 10 | 11 | func TestFormatTwoColumns(t *testing.T) { 12 | buf := bytes.NewBuffer(nil) 13 | formatTwoColumns(buf, 2, 2, 20, [][2]string{ 14 | {"--hello", "Hello world help with something that is cool."}, 15 | }) 16 | expected := ` --hello Hello 17 | world 18 | help with 19 | something 20 | that is 21 | cool. 22 | ` 23 | assert.Equal(t, expected, buf.String()) 24 | } 25 | 26 | func TestFormatTwoColumnsWide(t *testing.T) { 27 | samples := [][2]string{ 28 | {strings.Repeat("x", 29), "29 chars"}, 29 | {strings.Repeat("x", 30), "30 chars"}} 30 | buf := bytes.NewBuffer(nil) 31 | formatTwoColumns(buf, 0, 0, 200, samples) 32 | expected := `xxxxxxxxxxxxxxxxxxxxxxxxxxxxx29 chars 33 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 34 | 30 chars 35 | ` 36 | assert.Equal(t, expected, buf.String()) 37 | } 38 | 39 | func TestHiddenCommand(t *testing.T) { 40 | templates := []struct{ name, template string }{ 41 | {"default", DefaultUsageTemplate}, 42 | {"Compact", CompactUsageTemplate}, 43 | {"Long", LongHelpTemplate}, 44 | {"Man", ManPageTemplate}, 45 | } 46 | 47 | var buf bytes.Buffer 48 | t.Log("1") 49 | 50 | a := New("test", "Test").Writer(&buf).Terminate(nil) 51 | a.Command("visible", "visible") 52 | a.Command("hidden", "hidden").Hidden() 53 | 54 | for _, tp := range templates { 55 | buf.Reset() 56 | a.UsageTemplate(tp.template) 57 | a.Parse(nil) 58 | // a.Parse([]string{"--help"}) 59 | usage := buf.String() 60 | t.Logf("Usage for %s is:\n%s\n", tp.name, usage) 61 | 62 | assert.NotContains(t, usage, "hidden") 63 | assert.Contains(t, usage, "visible") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /values.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | //go:generate go run ./cmd/genvalues/main.go 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "net/url" 9 | "os" 10 | "reflect" 11 | "regexp" 12 | "strings" 13 | "time" 14 | 15 | "github.com/alecthomas/units" 16 | ) 17 | 18 | // NOTE: Most of the base type values were lifted from: 19 | // http://golang.org/src/pkg/flag/flag.go?s=20146:20222 20 | 21 | // Value is the interface to the dynamic value stored in a flag. 22 | // (The default value is represented as a string.) 23 | // 24 | // If a Value has an IsBoolFlag() bool method returning true, the command-line 25 | // parser makes --name equivalent to -name=true rather than using the next 26 | // command-line argument, and adds a --no-name counterpart for negating the 27 | // flag. 28 | type Value interface { 29 | String() string 30 | Set(string) error 31 | } 32 | 33 | // Getter is an interface that allows the contents of a Value to be retrieved. 34 | // It wraps the Value interface, rather than being part of it, because it 35 | // appeared after Go 1 and its compatibility rules. All Value types provided 36 | // by this package satisfy the Getter interface. 37 | type Getter interface { 38 | Value 39 | Get() interface{} 40 | } 41 | 42 | // Optional interface to indicate boolean flags that don't accept a value, and 43 | // implicitly have a --no- negation counterpart. 44 | type boolFlag interface { 45 | Value 46 | IsBoolFlag() bool 47 | } 48 | 49 | // Optional interface for arguments that cumulatively consume all remaining 50 | // input. 51 | type remainderArg interface { 52 | Value 53 | IsCumulative() bool 54 | } 55 | 56 | // Optional interface for flags that can be repeated. 57 | type repeatableFlag interface { 58 | Value 59 | IsCumulative() bool 60 | } 61 | 62 | type accumulator struct { 63 | element func(value interface{}) Value 64 | typ reflect.Type 65 | slice reflect.Value 66 | } 67 | 68 | // Use reflection to accumulate values into a slice. 69 | // 70 | // target := []string{} 71 | // newAccumulator(&target, func (value interface{}) Value { 72 | // return newStringValue(value.(*string)) 73 | // }) 74 | func newAccumulator(slice interface{}, element func(value interface{}) Value) *accumulator { 75 | typ := reflect.TypeOf(slice) 76 | if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Slice { 77 | panic("expected a pointer to a slice") 78 | } 79 | return &accumulator{ 80 | element: element, 81 | typ: typ.Elem().Elem(), 82 | slice: reflect.ValueOf(slice), 83 | } 84 | } 85 | 86 | func (a *accumulator) String() string { 87 | out := []string{} 88 | s := a.slice.Elem() 89 | for i := 0; i < s.Len(); i++ { 90 | out = append(out, a.element(s.Index(i).Addr().Interface()).String()) 91 | } 92 | return strings.Join(out, ",") 93 | } 94 | 95 | func (a *accumulator) Set(value string) error { 96 | e := reflect.New(a.typ) 97 | if err := a.element(e.Interface()).Set(value); err != nil { 98 | return err 99 | } 100 | slice := reflect.Append(a.slice.Elem(), e.Elem()) 101 | a.slice.Elem().Set(slice) 102 | return nil 103 | } 104 | 105 | func (a *accumulator) Get() interface{} { 106 | return a.slice.Interface() 107 | } 108 | 109 | func (a *accumulator) IsCumulative() bool { 110 | return true 111 | } 112 | 113 | func (b *boolValue) IsBoolFlag() bool { return true } 114 | 115 | // -- time.Duration Value 116 | type durationValue time.Duration 117 | 118 | func newDurationValue(p *time.Duration) *durationValue { 119 | return (*durationValue)(p) 120 | } 121 | 122 | func (d *durationValue) Set(s string) error { 123 | v, err := time.ParseDuration(s) 124 | *d = durationValue(v) 125 | return err 126 | } 127 | 128 | func (d *durationValue) Get() interface{} { return time.Duration(*d) } 129 | 130 | func (d *durationValue) String() string { return (*time.Duration)(d).String() } 131 | 132 | // -- map[string]string Value 133 | type stringMapValue map[string]string 134 | 135 | func newStringMapValue(p *map[string]string) *stringMapValue { 136 | return (*stringMapValue)(p) 137 | } 138 | 139 | var stringMapRegex = regexp.MustCompile("[:=]") 140 | 141 | func (s *stringMapValue) Set(value string) error { 142 | parts := stringMapRegex.Split(value, 2) 143 | if len(parts) != 2 { 144 | return fmt.Errorf("expected KEY=VALUE got '%s'", value) 145 | } 146 | (*s)[parts[0]] = parts[1] 147 | return nil 148 | } 149 | 150 | func (s *stringMapValue) Get() interface{} { 151 | return (map[string]string)(*s) 152 | } 153 | 154 | func (s *stringMapValue) String() string { 155 | return fmt.Sprintf("%s", map[string]string(*s)) 156 | } 157 | 158 | func (s *stringMapValue) IsCumulative() bool { 159 | return true 160 | } 161 | 162 | // -- net.IP Value 163 | type ipValue net.IP 164 | 165 | func newIPValue(p *net.IP) *ipValue { 166 | return (*ipValue)(p) 167 | } 168 | 169 | func (i *ipValue) Set(value string) error { 170 | if ip := net.ParseIP(value); ip == nil { 171 | return fmt.Errorf("'%s' is not an IP address", value) 172 | } else { 173 | *i = *(*ipValue)(&ip) 174 | return nil 175 | } 176 | } 177 | 178 | func (i *ipValue) Get() interface{} { 179 | return (net.IP)(*i) 180 | } 181 | 182 | func (i *ipValue) String() string { 183 | return (*net.IP)(i).String() 184 | } 185 | 186 | // -- *net.TCPAddr Value 187 | type tcpAddrValue struct { 188 | addr **net.TCPAddr 189 | } 190 | 191 | func newTCPAddrValue(p **net.TCPAddr) *tcpAddrValue { 192 | return &tcpAddrValue{p} 193 | } 194 | 195 | func (i *tcpAddrValue) Set(value string) error { 196 | if addr, err := net.ResolveTCPAddr("tcp", value); err != nil { 197 | return fmt.Errorf("'%s' is not a valid TCP address: %s", value, err) 198 | } else { 199 | *i.addr = addr 200 | return nil 201 | } 202 | } 203 | 204 | func (t *tcpAddrValue) Get() interface{} { 205 | return (*net.TCPAddr)(*t.addr) 206 | } 207 | 208 | func (i *tcpAddrValue) String() string { 209 | return (*i.addr).String() 210 | } 211 | 212 | // -- existingFile Value 213 | 214 | type fileStatValue struct { 215 | path *string 216 | predicate func(os.FileInfo) error 217 | } 218 | 219 | func newFileStatValue(p *string, predicate func(os.FileInfo) error) *fileStatValue { 220 | return &fileStatValue{ 221 | path: p, 222 | predicate: predicate, 223 | } 224 | } 225 | 226 | func (e *fileStatValue) Set(value string) error { 227 | if s, err := os.Stat(value); os.IsNotExist(err) { 228 | return fmt.Errorf("path '%s' does not exist", value) 229 | } else if err != nil { 230 | return err 231 | } else if err := e.predicate(s); err != nil { 232 | return err 233 | } 234 | *e.path = value 235 | return nil 236 | } 237 | 238 | func (f *fileStatValue) Get() interface{} { 239 | return (string)(*f.path) 240 | } 241 | 242 | func (e *fileStatValue) String() string { 243 | return *e.path 244 | } 245 | 246 | // -- os.File value 247 | 248 | type fileValue struct { 249 | f **os.File 250 | flag int 251 | perm os.FileMode 252 | } 253 | 254 | func newFileValue(p **os.File, flag int, perm os.FileMode) *fileValue { 255 | return &fileValue{p, flag, perm} 256 | } 257 | 258 | func (f *fileValue) Set(value string) error { 259 | if fd, err := os.OpenFile(value, f.flag, f.perm); err != nil { 260 | return err 261 | } else { 262 | *f.f = fd 263 | return nil 264 | } 265 | } 266 | 267 | func (f *fileValue) Get() interface{} { 268 | return (*os.File)(*f.f) 269 | } 270 | 271 | func (f *fileValue) String() string { 272 | if *f.f == nil { 273 | return "" 274 | } 275 | return (*f.f).Name() 276 | } 277 | 278 | // -- url.URL Value 279 | type urlValue struct { 280 | u **url.URL 281 | } 282 | 283 | func newURLValue(p **url.URL) *urlValue { 284 | return &urlValue{p} 285 | } 286 | 287 | func (u *urlValue) Set(value string) error { 288 | if url, err := url.Parse(value); err != nil { 289 | return fmt.Errorf("invalid URL: %s", err) 290 | } else { 291 | *u.u = url 292 | return nil 293 | } 294 | } 295 | 296 | func (u *urlValue) Get() interface{} { 297 | return (*url.URL)(*u.u) 298 | } 299 | 300 | func (u *urlValue) String() string { 301 | if *u.u == nil { 302 | return "" 303 | } 304 | return (*u.u).String() 305 | } 306 | 307 | // -- []*url.URL Value 308 | type urlListValue []*url.URL 309 | 310 | func newURLListValue(p *[]*url.URL) *urlListValue { 311 | return (*urlListValue)(p) 312 | } 313 | 314 | func (u *urlListValue) Set(value string) error { 315 | if url, err := url.Parse(value); err != nil { 316 | return fmt.Errorf("invalid URL: %s", err) 317 | } else { 318 | *u = append(*u, url) 319 | return nil 320 | } 321 | } 322 | 323 | func (u *urlListValue) Get() interface{} { 324 | return ([]*url.URL)(*u) 325 | } 326 | 327 | func (u *urlListValue) String() string { 328 | out := []string{} 329 | for _, url := range *u { 330 | out = append(out, url.String()) 331 | } 332 | return strings.Join(out, ",") 333 | } 334 | 335 | func (u *urlListValue) IsCumulative() bool { 336 | return true 337 | } 338 | 339 | // A flag whose value must be in a set of options. 340 | type enumValue struct { 341 | value *string 342 | options []string 343 | } 344 | 345 | func newEnumFlag(target *string, options ...string) *enumValue { 346 | return &enumValue{ 347 | value: target, 348 | options: options, 349 | } 350 | } 351 | 352 | func (a *enumValue) String() string { 353 | return *a.value 354 | } 355 | 356 | func (a *enumValue) Set(value string) error { 357 | for _, v := range a.options { 358 | if v == value { 359 | *a.value = value 360 | return nil 361 | } 362 | } 363 | return fmt.Errorf("enum value must be one of %s, got '%s'", strings.Join(a.options, ","), value) 364 | } 365 | 366 | func (e *enumValue) Get() interface{} { 367 | return (string)(*e.value) 368 | } 369 | 370 | // -- []string Enum Value 371 | type enumsValue struct { 372 | value *[]string 373 | options []string 374 | } 375 | 376 | func newEnumsFlag(target *[]string, options ...string) *enumsValue { 377 | return &enumsValue{ 378 | value: target, 379 | options: options, 380 | } 381 | } 382 | 383 | func (s *enumsValue) Set(value string) error { 384 | for _, v := range s.options { 385 | if v == value { 386 | *s.value = append(*s.value, value) 387 | return nil 388 | } 389 | } 390 | return fmt.Errorf("enum value must be one of %s, got '%s'", strings.Join(s.options, ","), value) 391 | } 392 | 393 | func (e *enumsValue) Get() interface{} { 394 | return ([]string)(*e.value) 395 | } 396 | 397 | func (s *enumsValue) String() string { 398 | return strings.Join(*s.value, ",") 399 | } 400 | 401 | func (s *enumsValue) IsCumulative() bool { 402 | return true 403 | } 404 | 405 | // -- units.Base2Bytes Value 406 | type bytesValue units.Base2Bytes 407 | 408 | func newBytesValue(p *units.Base2Bytes) *bytesValue { 409 | return (*bytesValue)(p) 410 | } 411 | 412 | func (d *bytesValue) Set(s string) error { 413 | v, err := units.ParseBase2Bytes(s) 414 | *d = bytesValue(v) 415 | return err 416 | } 417 | 418 | func (d *bytesValue) Get() interface{} { return units.Base2Bytes(*d) } 419 | 420 | func (d *bytesValue) String() string { return (*units.Base2Bytes)(d).String() } 421 | 422 | func newExistingFileValue(target *string) *fileStatValue { 423 | return newFileStatValue(target, func(s os.FileInfo) error { 424 | if s.IsDir() { 425 | return fmt.Errorf("'%s' is a directory", s.Name()) 426 | } 427 | return nil 428 | }) 429 | } 430 | 431 | func newExistingDirValue(target *string) *fileStatValue { 432 | return newFileStatValue(target, func(s os.FileInfo) error { 433 | if !s.IsDir() { 434 | return fmt.Errorf("'%s' is a file", s.Name()) 435 | } 436 | return nil 437 | }) 438 | } 439 | 440 | func newExistingFileOrDirValue(target *string) *fileStatValue { 441 | return newFileStatValue(target, func(s os.FileInfo) error { return nil }) 442 | } 443 | 444 | type counterValue int 445 | 446 | func newCounterValue(n *int) *counterValue { 447 | return (*counterValue)(n) 448 | } 449 | 450 | func (c *counterValue) Set(s string) error { 451 | *c++ 452 | return nil 453 | } 454 | 455 | func (c *counterValue) Get() interface{} { return (int)(*c) } 456 | func (c *counterValue) IsBoolFlag() bool { return true } 457 | func (c *counterValue) String() string { return fmt.Sprintf("%d", *c) } 458 | func (c *counterValue) IsCumulative() bool { return true } 459 | 460 | func resolveHost(value string) (net.IP, error) { 461 | if ip := net.ParseIP(value); ip != nil { 462 | return ip, nil 463 | } else { 464 | if addr, err := net.ResolveIPAddr("ip", value); err != nil { 465 | return nil, err 466 | } else { 467 | return addr.IP, nil 468 | } 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /values.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"type": "bool", "parser": "strconv.ParseBool(s)"}, 3 | {"type": "string", "parser": "s, error(nil)", "format": "string(*f.v)", "plural": "Strings"}, 4 | {"type": "uint", "parser": "strconv.ParseUint(s, 0, 64)", "plural": "Uints"}, 5 | {"type": "uint8", "parser": "strconv.ParseUint(s, 0, 8)"}, 6 | {"type": "uint16", "parser": "strconv.ParseUint(s, 0, 16)"}, 7 | {"type": "uint32", "parser": "strconv.ParseUint(s, 0, 32)"}, 8 | {"type": "uint64", "parser": "strconv.ParseUint(s, 0, 64)"}, 9 | {"type": "int", "parser": "strconv.ParseFloat(s, 64)", "plural": "Ints"}, 10 | {"type": "int8", "parser": "strconv.ParseInt(s, 0, 8)"}, 11 | {"type": "int16", "parser": "strconv.ParseInt(s, 0, 16)"}, 12 | {"type": "int32", "parser": "strconv.ParseInt(s, 0, 32)"}, 13 | {"type": "int64", "parser": "strconv.ParseInt(s, 0, 64)"}, 14 | {"type": "float64", "parser": "strconv.ParseFloat(s, 64)"}, 15 | {"type": "float32", "parser": "strconv.ParseFloat(s, 32)"}, 16 | {"name": "Duration", "type": "time.Duration", "no_value_parser": true}, 17 | {"name": "IP", "type": "net.IP", "no_value_parser": true}, 18 | {"name": "TCPAddr", "Type": "*net.TCPAddr", "plural": "TCPList", "no_value_parser": true}, 19 | {"name": "ExistingFile", "Type": "string", "plural": "ExistingFiles", "no_value_parser": true}, 20 | {"name": "ExistingDir", "Type": "string", "plural": "ExistingDirs", "no_value_parser": true}, 21 | {"name": "ExistingFileOrDir", "Type": "string", "plural": "ExistingFilesOrDirs", "no_value_parser": true}, 22 | {"name": "Regexp", "Type": "*regexp.Regexp", "parser": "regexp.Compile(s)"}, 23 | {"name": "ResolvedIP", "Type": "net.IP", "parser": "resolveHost(s)", "help": "Resolve a hostname or IP to an IP."}, 24 | {"name": "HexBytes", "Type": "[]byte", "parser": "hex.DecodeString(s)", "help": "Bytes as a hex string."} 25 | ] 26 | -------------------------------------------------------------------------------- /values_generated.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "net" 7 | "regexp" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | // This file is autogenerated by "go generate .". Do not modify. 13 | 14 | // -- bool Value 15 | type boolValue struct{ v *bool } 16 | 17 | func newBoolValue(p *bool) *boolValue { 18 | return &boolValue{p} 19 | } 20 | 21 | func (f *boolValue) Set(s string) error { 22 | v, err := strconv.ParseBool(s) 23 | if err == nil { 24 | *f.v = (bool)(v) 25 | } 26 | return err 27 | } 28 | 29 | func (f *boolValue) Get() interface{} { return (bool)(*f.v) } 30 | 31 | func (f *boolValue) String() string { return fmt.Sprintf("%v", *f) } 32 | 33 | // Bool parses the next command-line value as bool. 34 | func (p *parserMixin) Bool() (target *bool) { 35 | target = new(bool) 36 | p.BoolVar(target) 37 | return 38 | } 39 | 40 | func (p *parserMixin) BoolVar(target *bool) { 41 | p.SetValue(newBoolValue(target)) 42 | } 43 | 44 | // BoolList accumulates bool values into a slice. 45 | func (p *parserMixin) BoolList() (target *[]bool) { 46 | target = new([]bool) 47 | p.BoolListVar(target) 48 | return 49 | } 50 | 51 | func (p *parserMixin) BoolListVar(target *[]bool) { 52 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 53 | return newBoolValue(v.(*bool)) 54 | })) 55 | } 56 | 57 | // -- string Value 58 | type stringValue struct{ v *string } 59 | 60 | func newStringValue(p *string) *stringValue { 61 | return &stringValue{p} 62 | } 63 | 64 | func (f *stringValue) Set(s string) error { 65 | v, err := s, error(nil) 66 | if err == nil { 67 | *f.v = (string)(v) 68 | } 69 | return err 70 | } 71 | 72 | func (f *stringValue) Get() interface{} { return (string)(*f.v) } 73 | 74 | func (f *stringValue) String() string { return string(*f.v) } 75 | 76 | // String parses the next command-line value as string. 77 | func (p *parserMixin) String() (target *string) { 78 | target = new(string) 79 | p.StringVar(target) 80 | return 81 | } 82 | 83 | func (p *parserMixin) StringVar(target *string) { 84 | p.SetValue(newStringValue(target)) 85 | } 86 | 87 | // Strings accumulates string values into a slice. 88 | func (p *parserMixin) Strings() (target *[]string) { 89 | target = new([]string) 90 | p.StringsVar(target) 91 | return 92 | } 93 | 94 | func (p *parserMixin) StringsVar(target *[]string) { 95 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 96 | return newStringValue(v.(*string)) 97 | })) 98 | } 99 | 100 | // -- uint Value 101 | type uintValue struct{ v *uint } 102 | 103 | func newUintValue(p *uint) *uintValue { 104 | return &uintValue{p} 105 | } 106 | 107 | func (f *uintValue) Set(s string) error { 108 | v, err := strconv.ParseUint(s, 0, 64) 109 | if err == nil { 110 | *f.v = (uint)(v) 111 | } 112 | return err 113 | } 114 | 115 | func (f *uintValue) Get() interface{} { return (uint)(*f.v) } 116 | 117 | func (f *uintValue) String() string { return fmt.Sprintf("%v", *f) } 118 | 119 | // Uint parses the next command-line value as uint. 120 | func (p *parserMixin) Uint() (target *uint) { 121 | target = new(uint) 122 | p.UintVar(target) 123 | return 124 | } 125 | 126 | func (p *parserMixin) UintVar(target *uint) { 127 | p.SetValue(newUintValue(target)) 128 | } 129 | 130 | // Uints accumulates uint values into a slice. 131 | func (p *parserMixin) Uints() (target *[]uint) { 132 | target = new([]uint) 133 | p.UintsVar(target) 134 | return 135 | } 136 | 137 | func (p *parserMixin) UintsVar(target *[]uint) { 138 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 139 | return newUintValue(v.(*uint)) 140 | })) 141 | } 142 | 143 | // -- uint8 Value 144 | type uint8Value struct{ v *uint8 } 145 | 146 | func newUint8Value(p *uint8) *uint8Value { 147 | return &uint8Value{p} 148 | } 149 | 150 | func (f *uint8Value) Set(s string) error { 151 | v, err := strconv.ParseUint(s, 0, 8) 152 | if err == nil { 153 | *f.v = (uint8)(v) 154 | } 155 | return err 156 | } 157 | 158 | func (f *uint8Value) Get() interface{} { return (uint8)(*f.v) } 159 | 160 | func (f *uint8Value) String() string { return fmt.Sprintf("%v", *f) } 161 | 162 | // Uint8 parses the next command-line value as uint8. 163 | func (p *parserMixin) Uint8() (target *uint8) { 164 | target = new(uint8) 165 | p.Uint8Var(target) 166 | return 167 | } 168 | 169 | func (p *parserMixin) Uint8Var(target *uint8) { 170 | p.SetValue(newUint8Value(target)) 171 | } 172 | 173 | // Uint8List accumulates uint8 values into a slice. 174 | func (p *parserMixin) Uint8List() (target *[]uint8) { 175 | target = new([]uint8) 176 | p.Uint8ListVar(target) 177 | return 178 | } 179 | 180 | func (p *parserMixin) Uint8ListVar(target *[]uint8) { 181 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 182 | return newUint8Value(v.(*uint8)) 183 | })) 184 | } 185 | 186 | // -- uint16 Value 187 | type uint16Value struct{ v *uint16 } 188 | 189 | func newUint16Value(p *uint16) *uint16Value { 190 | return &uint16Value{p} 191 | } 192 | 193 | func (f *uint16Value) Set(s string) error { 194 | v, err := strconv.ParseUint(s, 0, 16) 195 | if err == nil { 196 | *f.v = (uint16)(v) 197 | } 198 | return err 199 | } 200 | 201 | func (f *uint16Value) Get() interface{} { return (uint16)(*f.v) } 202 | 203 | func (f *uint16Value) String() string { return fmt.Sprintf("%v", *f) } 204 | 205 | // Uint16 parses the next command-line value as uint16. 206 | func (p *parserMixin) Uint16() (target *uint16) { 207 | target = new(uint16) 208 | p.Uint16Var(target) 209 | return 210 | } 211 | 212 | func (p *parserMixin) Uint16Var(target *uint16) { 213 | p.SetValue(newUint16Value(target)) 214 | } 215 | 216 | // Uint16List accumulates uint16 values into a slice. 217 | func (p *parserMixin) Uint16List() (target *[]uint16) { 218 | target = new([]uint16) 219 | p.Uint16ListVar(target) 220 | return 221 | } 222 | 223 | func (p *parserMixin) Uint16ListVar(target *[]uint16) { 224 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 225 | return newUint16Value(v.(*uint16)) 226 | })) 227 | } 228 | 229 | // -- uint32 Value 230 | type uint32Value struct{ v *uint32 } 231 | 232 | func newUint32Value(p *uint32) *uint32Value { 233 | return &uint32Value{p} 234 | } 235 | 236 | func (f *uint32Value) Set(s string) error { 237 | v, err := strconv.ParseUint(s, 0, 32) 238 | if err == nil { 239 | *f.v = (uint32)(v) 240 | } 241 | return err 242 | } 243 | 244 | func (f *uint32Value) Get() interface{} { return (uint32)(*f.v) } 245 | 246 | func (f *uint32Value) String() string { return fmt.Sprintf("%v", *f) } 247 | 248 | // Uint32 parses the next command-line value as uint32. 249 | func (p *parserMixin) Uint32() (target *uint32) { 250 | target = new(uint32) 251 | p.Uint32Var(target) 252 | return 253 | } 254 | 255 | func (p *parserMixin) Uint32Var(target *uint32) { 256 | p.SetValue(newUint32Value(target)) 257 | } 258 | 259 | // Uint32List accumulates uint32 values into a slice. 260 | func (p *parserMixin) Uint32List() (target *[]uint32) { 261 | target = new([]uint32) 262 | p.Uint32ListVar(target) 263 | return 264 | } 265 | 266 | func (p *parserMixin) Uint32ListVar(target *[]uint32) { 267 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 268 | return newUint32Value(v.(*uint32)) 269 | })) 270 | } 271 | 272 | // -- uint64 Value 273 | type uint64Value struct{ v *uint64 } 274 | 275 | func newUint64Value(p *uint64) *uint64Value { 276 | return &uint64Value{p} 277 | } 278 | 279 | func (f *uint64Value) Set(s string) error { 280 | v, err := strconv.ParseUint(s, 0, 64) 281 | if err == nil { 282 | *f.v = (uint64)(v) 283 | } 284 | return err 285 | } 286 | 287 | func (f *uint64Value) Get() interface{} { return (uint64)(*f.v) } 288 | 289 | func (f *uint64Value) String() string { return fmt.Sprintf("%v", *f) } 290 | 291 | // Uint64 parses the next command-line value as uint64. 292 | func (p *parserMixin) Uint64() (target *uint64) { 293 | target = new(uint64) 294 | p.Uint64Var(target) 295 | return 296 | } 297 | 298 | func (p *parserMixin) Uint64Var(target *uint64) { 299 | p.SetValue(newUint64Value(target)) 300 | } 301 | 302 | // Uint64List accumulates uint64 values into a slice. 303 | func (p *parserMixin) Uint64List() (target *[]uint64) { 304 | target = new([]uint64) 305 | p.Uint64ListVar(target) 306 | return 307 | } 308 | 309 | func (p *parserMixin) Uint64ListVar(target *[]uint64) { 310 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 311 | return newUint64Value(v.(*uint64)) 312 | })) 313 | } 314 | 315 | // -- int Value 316 | type intValue struct{ v *int } 317 | 318 | func newIntValue(p *int) *intValue { 319 | return &intValue{p} 320 | } 321 | 322 | func (f *intValue) Set(s string) error { 323 | v, err := strconv.ParseFloat(s, 64) 324 | if err == nil { 325 | *f.v = (int)(v) 326 | } 327 | return err 328 | } 329 | 330 | func (f *intValue) Get() interface{} { return (int)(*f.v) } 331 | 332 | func (f *intValue) String() string { return fmt.Sprintf("%v", *f) } 333 | 334 | // Int parses the next command-line value as int. 335 | func (p *parserMixin) Int() (target *int) { 336 | target = new(int) 337 | p.IntVar(target) 338 | return 339 | } 340 | 341 | func (p *parserMixin) IntVar(target *int) { 342 | p.SetValue(newIntValue(target)) 343 | } 344 | 345 | // Ints accumulates int values into a slice. 346 | func (p *parserMixin) Ints() (target *[]int) { 347 | target = new([]int) 348 | p.IntsVar(target) 349 | return 350 | } 351 | 352 | func (p *parserMixin) IntsVar(target *[]int) { 353 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 354 | return newIntValue(v.(*int)) 355 | })) 356 | } 357 | 358 | // -- int8 Value 359 | type int8Value struct{ v *int8 } 360 | 361 | func newInt8Value(p *int8) *int8Value { 362 | return &int8Value{p} 363 | } 364 | 365 | func (f *int8Value) Set(s string) error { 366 | v, err := strconv.ParseInt(s, 0, 8) 367 | if err == nil { 368 | *f.v = (int8)(v) 369 | } 370 | return err 371 | } 372 | 373 | func (f *int8Value) Get() interface{} { return (int8)(*f.v) } 374 | 375 | func (f *int8Value) String() string { return fmt.Sprintf("%v", *f) } 376 | 377 | // Int8 parses the next command-line value as int8. 378 | func (p *parserMixin) Int8() (target *int8) { 379 | target = new(int8) 380 | p.Int8Var(target) 381 | return 382 | } 383 | 384 | func (p *parserMixin) Int8Var(target *int8) { 385 | p.SetValue(newInt8Value(target)) 386 | } 387 | 388 | // Int8List accumulates int8 values into a slice. 389 | func (p *parserMixin) Int8List() (target *[]int8) { 390 | target = new([]int8) 391 | p.Int8ListVar(target) 392 | return 393 | } 394 | 395 | func (p *parserMixin) Int8ListVar(target *[]int8) { 396 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 397 | return newInt8Value(v.(*int8)) 398 | })) 399 | } 400 | 401 | // -- int16 Value 402 | type int16Value struct{ v *int16 } 403 | 404 | func newInt16Value(p *int16) *int16Value { 405 | return &int16Value{p} 406 | } 407 | 408 | func (f *int16Value) Set(s string) error { 409 | v, err := strconv.ParseInt(s, 0, 16) 410 | if err == nil { 411 | *f.v = (int16)(v) 412 | } 413 | return err 414 | } 415 | 416 | func (f *int16Value) Get() interface{} { return (int16)(*f.v) } 417 | 418 | func (f *int16Value) String() string { return fmt.Sprintf("%v", *f) } 419 | 420 | // Int16 parses the next command-line value as int16. 421 | func (p *parserMixin) Int16() (target *int16) { 422 | target = new(int16) 423 | p.Int16Var(target) 424 | return 425 | } 426 | 427 | func (p *parserMixin) Int16Var(target *int16) { 428 | p.SetValue(newInt16Value(target)) 429 | } 430 | 431 | // Int16List accumulates int16 values into a slice. 432 | func (p *parserMixin) Int16List() (target *[]int16) { 433 | target = new([]int16) 434 | p.Int16ListVar(target) 435 | return 436 | } 437 | 438 | func (p *parserMixin) Int16ListVar(target *[]int16) { 439 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 440 | return newInt16Value(v.(*int16)) 441 | })) 442 | } 443 | 444 | // -- int32 Value 445 | type int32Value struct{ v *int32 } 446 | 447 | func newInt32Value(p *int32) *int32Value { 448 | return &int32Value{p} 449 | } 450 | 451 | func (f *int32Value) Set(s string) error { 452 | v, err := strconv.ParseInt(s, 0, 32) 453 | if err == nil { 454 | *f.v = (int32)(v) 455 | } 456 | return err 457 | } 458 | 459 | func (f *int32Value) Get() interface{} { return (int32)(*f.v) } 460 | 461 | func (f *int32Value) String() string { return fmt.Sprintf("%v", *f) } 462 | 463 | // Int32 parses the next command-line value as int32. 464 | func (p *parserMixin) Int32() (target *int32) { 465 | target = new(int32) 466 | p.Int32Var(target) 467 | return 468 | } 469 | 470 | func (p *parserMixin) Int32Var(target *int32) { 471 | p.SetValue(newInt32Value(target)) 472 | } 473 | 474 | // Int32List accumulates int32 values into a slice. 475 | func (p *parserMixin) Int32List() (target *[]int32) { 476 | target = new([]int32) 477 | p.Int32ListVar(target) 478 | return 479 | } 480 | 481 | func (p *parserMixin) Int32ListVar(target *[]int32) { 482 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 483 | return newInt32Value(v.(*int32)) 484 | })) 485 | } 486 | 487 | // -- int64 Value 488 | type int64Value struct{ v *int64 } 489 | 490 | func newInt64Value(p *int64) *int64Value { 491 | return &int64Value{p} 492 | } 493 | 494 | func (f *int64Value) Set(s string) error { 495 | v, err := strconv.ParseInt(s, 0, 64) 496 | if err == nil { 497 | *f.v = (int64)(v) 498 | } 499 | return err 500 | } 501 | 502 | func (f *int64Value) Get() interface{} { return (int64)(*f.v) } 503 | 504 | func (f *int64Value) String() string { return fmt.Sprintf("%v", *f) } 505 | 506 | // Int64 parses the next command-line value as int64. 507 | func (p *parserMixin) Int64() (target *int64) { 508 | target = new(int64) 509 | p.Int64Var(target) 510 | return 511 | } 512 | 513 | func (p *parserMixin) Int64Var(target *int64) { 514 | p.SetValue(newInt64Value(target)) 515 | } 516 | 517 | // Int64List accumulates int64 values into a slice. 518 | func (p *parserMixin) Int64List() (target *[]int64) { 519 | target = new([]int64) 520 | p.Int64ListVar(target) 521 | return 522 | } 523 | 524 | func (p *parserMixin) Int64ListVar(target *[]int64) { 525 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 526 | return newInt64Value(v.(*int64)) 527 | })) 528 | } 529 | 530 | // -- float64 Value 531 | type float64Value struct{ v *float64 } 532 | 533 | func newFloat64Value(p *float64) *float64Value { 534 | return &float64Value{p} 535 | } 536 | 537 | func (f *float64Value) Set(s string) error { 538 | v, err := strconv.ParseFloat(s, 64) 539 | if err == nil { 540 | *f.v = (float64)(v) 541 | } 542 | return err 543 | } 544 | 545 | func (f *float64Value) Get() interface{} { return (float64)(*f.v) } 546 | 547 | func (f *float64Value) String() string { return fmt.Sprintf("%v", *f) } 548 | 549 | // Float64 parses the next command-line value as float64. 550 | func (p *parserMixin) Float64() (target *float64) { 551 | target = new(float64) 552 | p.Float64Var(target) 553 | return 554 | } 555 | 556 | func (p *parserMixin) Float64Var(target *float64) { 557 | p.SetValue(newFloat64Value(target)) 558 | } 559 | 560 | // Float64List accumulates float64 values into a slice. 561 | func (p *parserMixin) Float64List() (target *[]float64) { 562 | target = new([]float64) 563 | p.Float64ListVar(target) 564 | return 565 | } 566 | 567 | func (p *parserMixin) Float64ListVar(target *[]float64) { 568 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 569 | return newFloat64Value(v.(*float64)) 570 | })) 571 | } 572 | 573 | // -- float32 Value 574 | type float32Value struct{ v *float32 } 575 | 576 | func newFloat32Value(p *float32) *float32Value { 577 | return &float32Value{p} 578 | } 579 | 580 | func (f *float32Value) Set(s string) error { 581 | v, err := strconv.ParseFloat(s, 32) 582 | if err == nil { 583 | *f.v = (float32)(v) 584 | } 585 | return err 586 | } 587 | 588 | func (f *float32Value) Get() interface{} { return (float32)(*f.v) } 589 | 590 | func (f *float32Value) String() string { return fmt.Sprintf("%v", *f) } 591 | 592 | // Float32 parses the next command-line value as float32. 593 | func (p *parserMixin) Float32() (target *float32) { 594 | target = new(float32) 595 | p.Float32Var(target) 596 | return 597 | } 598 | 599 | func (p *parserMixin) Float32Var(target *float32) { 600 | p.SetValue(newFloat32Value(target)) 601 | } 602 | 603 | // Float32List accumulates float32 values into a slice. 604 | func (p *parserMixin) Float32List() (target *[]float32) { 605 | target = new([]float32) 606 | p.Float32ListVar(target) 607 | return 608 | } 609 | 610 | func (p *parserMixin) Float32ListVar(target *[]float32) { 611 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 612 | return newFloat32Value(v.(*float32)) 613 | })) 614 | } 615 | 616 | // DurationList accumulates time.Duration values into a slice. 617 | func (p *parserMixin) DurationList() (target *[]time.Duration) { 618 | target = new([]time.Duration) 619 | p.DurationListVar(target) 620 | return 621 | } 622 | 623 | func (p *parserMixin) DurationListVar(target *[]time.Duration) { 624 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 625 | return newDurationValue(v.(*time.Duration)) 626 | })) 627 | } 628 | 629 | // IPList accumulates net.IP values into a slice. 630 | func (p *parserMixin) IPList() (target *[]net.IP) { 631 | target = new([]net.IP) 632 | p.IPListVar(target) 633 | return 634 | } 635 | 636 | func (p *parserMixin) IPListVar(target *[]net.IP) { 637 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 638 | return newIPValue(v.(*net.IP)) 639 | })) 640 | } 641 | 642 | // TCPList accumulates *net.TCPAddr values into a slice. 643 | func (p *parserMixin) TCPList() (target *[]*net.TCPAddr) { 644 | target = new([]*net.TCPAddr) 645 | p.TCPListVar(target) 646 | return 647 | } 648 | 649 | func (p *parserMixin) TCPListVar(target *[]*net.TCPAddr) { 650 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 651 | return newTCPAddrValue(v.(**net.TCPAddr)) 652 | })) 653 | } 654 | 655 | // ExistingFiles accumulates string values into a slice. 656 | func (p *parserMixin) ExistingFiles() (target *[]string) { 657 | target = new([]string) 658 | p.ExistingFilesVar(target) 659 | return 660 | } 661 | 662 | func (p *parserMixin) ExistingFilesVar(target *[]string) { 663 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 664 | return newExistingFileValue(v.(*string)) 665 | })) 666 | } 667 | 668 | // ExistingDirs accumulates string values into a slice. 669 | func (p *parserMixin) ExistingDirs() (target *[]string) { 670 | target = new([]string) 671 | p.ExistingDirsVar(target) 672 | return 673 | } 674 | 675 | func (p *parserMixin) ExistingDirsVar(target *[]string) { 676 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 677 | return newExistingDirValue(v.(*string)) 678 | })) 679 | } 680 | 681 | // ExistingFilesOrDirs accumulates string values into a slice. 682 | func (p *parserMixin) ExistingFilesOrDirs() (target *[]string) { 683 | target = new([]string) 684 | p.ExistingFilesOrDirsVar(target) 685 | return 686 | } 687 | 688 | func (p *parserMixin) ExistingFilesOrDirsVar(target *[]string) { 689 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 690 | return newExistingFileOrDirValue(v.(*string)) 691 | })) 692 | } 693 | 694 | // -- *regexp.Regexp Value 695 | type regexpValue struct{ v **regexp.Regexp } 696 | 697 | func newRegexpValue(p **regexp.Regexp) *regexpValue { 698 | return ®expValue{p} 699 | } 700 | 701 | func (f *regexpValue) Set(s string) error { 702 | v, err := regexp.Compile(s) 703 | if err == nil { 704 | *f.v = (*regexp.Regexp)(v) 705 | } 706 | return err 707 | } 708 | 709 | func (f *regexpValue) Get() interface{} { return (*regexp.Regexp)(*f.v) } 710 | 711 | func (f *regexpValue) String() string { return fmt.Sprintf("%v", *f) } 712 | 713 | // Regexp parses the next command-line value as *regexp.Regexp. 714 | func (p *parserMixin) Regexp() (target **regexp.Regexp) { 715 | target = new(*regexp.Regexp) 716 | p.RegexpVar(target) 717 | return 718 | } 719 | 720 | func (p *parserMixin) RegexpVar(target **regexp.Regexp) { 721 | p.SetValue(newRegexpValue(target)) 722 | } 723 | 724 | // RegexpList accumulates *regexp.Regexp values into a slice. 725 | func (p *parserMixin) RegexpList() (target *[]*regexp.Regexp) { 726 | target = new([]*regexp.Regexp) 727 | p.RegexpListVar(target) 728 | return 729 | } 730 | 731 | func (p *parserMixin) RegexpListVar(target *[]*regexp.Regexp) { 732 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 733 | return newRegexpValue(v.(**regexp.Regexp)) 734 | })) 735 | } 736 | 737 | // -- net.IP Value 738 | type resolvedIPValue struct{ v *net.IP } 739 | 740 | func newResolvedIPValue(p *net.IP) *resolvedIPValue { 741 | return &resolvedIPValue{p} 742 | } 743 | 744 | func (f *resolvedIPValue) Set(s string) error { 745 | v, err := resolveHost(s) 746 | if err == nil { 747 | *f.v = (net.IP)(v) 748 | } 749 | return err 750 | } 751 | 752 | func (f *resolvedIPValue) Get() interface{} { return (net.IP)(*f.v) } 753 | 754 | func (f *resolvedIPValue) String() string { return fmt.Sprintf("%v", *f) } 755 | 756 | // Resolve a hostname or IP to an IP. 757 | func (p *parserMixin) ResolvedIP() (target *net.IP) { 758 | target = new(net.IP) 759 | p.ResolvedIPVar(target) 760 | return 761 | } 762 | 763 | func (p *parserMixin) ResolvedIPVar(target *net.IP) { 764 | p.SetValue(newResolvedIPValue(target)) 765 | } 766 | 767 | // ResolvedIPList accumulates net.IP values into a slice. 768 | func (p *parserMixin) ResolvedIPList() (target *[]net.IP) { 769 | target = new([]net.IP) 770 | p.ResolvedIPListVar(target) 771 | return 772 | } 773 | 774 | func (p *parserMixin) ResolvedIPListVar(target *[]net.IP) { 775 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 776 | return newResolvedIPValue(v.(*net.IP)) 777 | })) 778 | } 779 | 780 | // -- []byte Value 781 | type hexBytesValue struct{ v *[]byte } 782 | 783 | func newHexBytesValue(p *[]byte) *hexBytesValue { 784 | return &hexBytesValue{p} 785 | } 786 | 787 | func (f *hexBytesValue) Set(s string) error { 788 | v, err := hex.DecodeString(s) 789 | if err == nil { 790 | *f.v = ([]byte)(v) 791 | } 792 | return err 793 | } 794 | 795 | func (f *hexBytesValue) Get() interface{} { return ([]byte)(*f.v) } 796 | 797 | func (f *hexBytesValue) String() string { return fmt.Sprintf("%v", *f) } 798 | 799 | // Bytes as a hex string. 800 | func (p *parserMixin) HexBytes() (target *[]byte) { 801 | target = new([]byte) 802 | p.HexBytesVar(target) 803 | return 804 | } 805 | 806 | func (p *parserMixin) HexBytesVar(target *[]byte) { 807 | p.SetValue(newHexBytesValue(target)) 808 | } 809 | 810 | // HexBytesList accumulates []byte values into a slice. 811 | func (p *parserMixin) HexBytesList() (target *[][]byte) { 812 | target = new([][]byte) 813 | p.HexBytesListVar(target) 814 | return 815 | } 816 | 817 | func (p *parserMixin) HexBytesListVar(target *[][]byte) { 818 | p.SetValue(newAccumulator(target, func(v interface{}) Value { 819 | return newHexBytesValue(v.(*[]byte)) 820 | })) 821 | } 822 | -------------------------------------------------------------------------------- /values_test.go: -------------------------------------------------------------------------------- 1 | package kingpin 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/alecthomas/assert" 7 | 8 | "testing" 9 | ) 10 | 11 | func TestAccumulatorStrings(t *testing.T) { 12 | target := []string{} 13 | acc := newAccumulator(&target, func(v interface{}) Value { return newStringValue(v.(*string)) }) 14 | acc.Set("a") 15 | assert.Equal(t, []string{"a"}, target) 16 | acc.Set("b") 17 | assert.Equal(t, []string{"a", "b"}, target) 18 | } 19 | 20 | func TestStrings(t *testing.T) { 21 | app := New("", "") 22 | app.Arg("a", "").Required().String() 23 | app.Arg("b", "").Required().String() 24 | c := app.Arg("c", "").Required().Strings() 25 | app.Parse([]string{"a", "b", "a", "b"}) 26 | assert.Equal(t, []string{"a", "b"}, *c) 27 | } 28 | 29 | func TestEnum(t *testing.T) { 30 | app := New("", "") 31 | a := app.Arg("a", "").Enum("one", "two", "three") 32 | _, err := app.Parse([]string{"moo"}) 33 | assert.Error(t, err) 34 | _, err = app.Parse([]string{"one"}) 35 | assert.NoError(t, err) 36 | assert.Equal(t, "one", *a) 37 | } 38 | 39 | func TestEnumVar(t *testing.T) { 40 | app := New("", "") 41 | var a string 42 | app.Arg("a", "").EnumVar(&a, "one", "two", "three") 43 | _, err := app.Parse([]string{"moo"}) 44 | assert.Error(t, err) 45 | _, err = app.Parse([]string{"one"}) 46 | assert.NoError(t, err) 47 | assert.Equal(t, "one", a) 48 | } 49 | 50 | func TestCounter(t *testing.T) { 51 | app := New("", "") 52 | c := app.Flag("f", "").Counter() 53 | _, err := app.Parse([]string{"--f", "--f", "--f"}) 54 | assert.NoError(t, err) 55 | assert.Equal(t, 3, *c) 56 | } 57 | 58 | func TestIPv4Addr(t *testing.T) { 59 | app := newTestApp() 60 | flag := app.Flag("addr", "").ResolvedIP() 61 | _, err := app.Parse([]string{"--addr", net.IPv4(1, 2, 3, 4).String()}) 62 | assert.NoError(t, err) 63 | assert.NotNil(t, *flag) 64 | assert.Equal(t, net.IPv4(1, 2, 3, 4), *flag) 65 | } 66 | 67 | func TestInvalidIPv4Addr(t *testing.T) { 68 | app := newTestApp() 69 | app.Flag("addr", "").ResolvedIP() 70 | _, err := app.Parse([]string{"--addr", "1.2.3.256"}) 71 | assert.Error(t, err) 72 | } 73 | 74 | func TestIPv6Addr(t *testing.T) { 75 | app := newTestApp() 76 | flag := app.Flag("addr", "").ResolvedIP() 77 | _, err := app.Parse([]string{"--addr", net.IPv6interfacelocalallnodes.String()}) 78 | assert.NoError(t, err) 79 | assert.NotNil(t, *flag) 80 | assert.Equal(t, net.IPv6interfacelocalallnodes, *flag) 81 | } 82 | 83 | func TestHexBytes(t *testing.T) { 84 | app := newTestApp() 85 | actual := app.Arg("bytes", "").HexBytes() 86 | _, err := app.Parse([]string{"01020aff"}) 87 | assert.NoError(t, err) 88 | assert.Equal(t, []byte{0x01, 0x02, 0x0a, 0xff}, *actual) 89 | } 90 | 91 | func TestSetValueDoesNotReset(t *testing.T) { 92 | app := newTestApp() 93 | mapping := map[string]string{ 94 | "key": "value", 95 | } 96 | app.Flag("set", "").StringMapVar(&mapping) 97 | assert.NotEmpty(t, mapping) 98 | } 99 | --------------------------------------------------------------------------------