├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── app.go ├── app_test.go ├── build_info.go ├── build_info_test.go ├── command.go ├── command_line_parser.go ├── command_line_parser_test.go ├── command_test.go ├── context.go ├── context_test.go ├── flag.go ├── flag_bool.go ├── flag_float32.go ├── flag_float64.go ├── flag_float64_slice.go ├── flag_int.go ├── flag_int16.go ├── flag_int32.go ├── flag_int64.go ├── flag_int8.go ├── flag_int_slice.go ├── flag_ip.go ├── flag_ip_mask.go ├── flag_ip_net.go ├── flag_ip_net_slice.go ├── flag_ip_slice.go ├── flag_string.go ├── flag_string_slice.go ├── flag_test.go ├── flag_time.go ├── flag_time_duration.go ├── flag_time_location.go ├── flag_uint.go ├── flag_uint16.go ├── flag_uint32.go ├── flag_uint64.go ├── flag_uint8.go ├── flag_uint_slice.go ├── flag_url.go ├── flag_url_slice.go ├── flag_value_test.go ├── go.mod ├── help.go └── help_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # -------------------------------------- 2 | # mac 3 | # -------------------------------------- 4 | .DS_Store 5 | 6 | # -------------------------------------- 7 | # idea / golang 8 | # -------------------------------------- 9 | .idea/ 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.13" 5 | - "tip" 6 | 7 | before_install: 8 | - go get -d github.com/mattn/goveralls 9 | - go install github.com/mattn/goveralls 10 | 11 | script: 12 | - $GOPATH/bin/goveralls -v -service=travis-ci 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-cli 2 | 3 | [![GoDoc](https://godoc.org/github.com/subchen/go-cli?status.svg)](https://godoc.org/github.com/subchen/go-cli) 4 | [![Build Status](https://travis-ci.org/subchen/go-cli.svg?branch=master)](https://travis-ci.org/subchen/go-cli) 5 | [![Coverage Status](https://coveralls.io/repos/github/subchen/go-cli/badge.svg?branch=master)](https://coveralls.io/github/subchen/go-cli?branch=master) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/subchen/go-cli)](https://goreportcard.com/report/github.com/subchen/go-cli) 7 | [![Release](https://img.shields.io/github/release/subchen/go-cli.svg)](https://github.com/subchen/go-cli/releases/latest) 8 | [![License](http://img.shields.io/badge/License-Apache_2-red.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) 9 | 10 | `go-cli` is a package to build a CLI application. Support command/sub-commands. 11 | 12 | 13 | Some applications are built using `go-cli` including: 14 | 15 | - [frep](https://github.com/subchen/frep) 16 | - [mknovel](https://github.com/subchen/mknovel) 17 | - [ovfenv-installer](https://github.com/subchen/ovfenv-installer) 18 | - [publish-toolset](https://github.com/subchen/publish-toolset) 19 | 20 | 21 | **Table of Contents** 22 | 23 | - [Installation](#overview) 24 | - [Syntax for Command Line](#syntax-for-command-line) 25 | - [Getting Started](#getting-started) 26 | * [Arguments](#arguments) 27 | * [Flags](#flags) 28 | + [Bool flag](#bool-flag) 29 | + [Value bind](#value-bind) 30 | + [Short, Long, Alias Names](#short-long-alias-names) 31 | + [Placeholder](#placeholder) 32 | + [Default Value](#default-value) 33 | + [NoOptDefVal](#nooptdefval) 34 | + [Hidden flags](#hidden-flags) 35 | * [Commands](#commands) 36 | - [Generate Help](#generate-help) 37 | * [Customize help](#customize-help) 38 | - [Generate Version](#generate-version) 39 | * [Customize version](#customize-version) 40 | - [Error Handler](#error-handler) 41 | * [OnCommandNotFound](#oncommandnotfound) 42 | * [OnActionPanic](#onactionpanic) 43 | - [Contributing](#contributing) 44 | - [License](#license) 45 | 46 | 47 | ## Installation 48 | 49 | `go-cli` is available using the standard go get command. 50 | 51 | To install `go-cli`, simply run: 52 | 53 | ```bash 54 | go get github.com/subchen/go-cli/v3 55 | ``` 56 | 57 | ## Syntax for Command Line 58 | 59 | ``` 60 | // Long option 61 | --flag // boolean flags, or flags with no option default values 62 | --flag x // only on flags without a default value 63 | --flag=x 64 | 65 | // Short option 66 | -x // boolean flags 67 | -x 123 68 | -x=123 69 | -x123 // value is 123 70 | 71 | // value wrapped by quote 72 | -x="123" 73 | -x='123' 74 | 75 | // unordered in flags and arguments 76 | arg1 -x 123 arg2 --test arg3 arg4 77 | 78 | // stops parsing after the terminator `--` 79 | -x 123 -- arg1 --not-a-flag arg3 arg4 80 | ``` 81 | 82 | 83 | ## Getting Started 84 | 85 | A simple CLI application: 86 | 87 | ```go 88 | package main 89 | 90 | import ( 91 | "fmt" 92 | "os" 93 | "github.com/subchen/go-cli/v3" 94 | ) 95 | 96 | func main() { 97 | app := cli.NewApp() 98 | app.Name = "hello" 99 | app.Version = "1.0.0" 100 | app.Usage = "a hello world application." 101 | app.Action = func(c *cli.Context) { 102 | fmt.Println("Hello World!") 103 | } 104 | app.Run(os.Args) 105 | } 106 | ``` 107 | 108 | Build and run our new CLI application 109 | 110 | ```bash 111 | $ go build 112 | $ ./hello 113 | Hello World! 114 | ``` 115 | 116 | `go-cli` also generates neat help text 117 | 118 | ```bash 119 | $ ./hello --help 120 | NAME: 121 | hello - a hello world application. 122 | 123 | USAGE: 124 | hello [options] [arguments...] 125 | 126 | VERSION: 127 | 1.0.0 128 | 129 | OPTIONS: 130 | --help print this help 131 | --version print version information 132 | ``` 133 | 134 | ### Arguments 135 | 136 | You can lookup arguments by calling the `Args` function on `cli.Context`, e.g.: 137 | 138 | ```go 139 | app := cli.NewApp() 140 | 141 | app.Action = func(c *cli.Context) { 142 | name := c.Args()[0] 143 | fmt.Printf("Hello %v\n", name) 144 | } 145 | 146 | app.Run(os.Args) 147 | ``` 148 | 149 | ### Flags 150 | 151 | Setting and querying flags is simple. 152 | 153 | 154 | ```go 155 | app := cli.NewApp() 156 | 157 | app.Flags = []*cli.Flag { 158 | { 159 | Name: "name", 160 | Usage: "a name of user", 161 | }, 162 | } 163 | 164 | app.Action = func(c *cli.Context) { 165 | name := c.GetString("name") 166 | fmt.Printf("Hello %v\n", name) 167 | } 168 | 169 | app.Run(os.Args) 170 | ``` 171 | 172 | #### Bool flag 173 | 174 | A bool flag can has a optional inline bool value. 175 | 176 | ```go 177 | &cli.Flag{ 178 | Name: "verbose", 179 | Usage: "output verbose information", 180 | IsBool: true, 181 | }, 182 | ``` 183 | 184 | The parsed arguments likes: 185 | 186 | ``` 187 | // valid 188 | --verbose 189 | --verbose=true 190 | --verbose=false 191 | 192 | // invalid 193 | --verbose false 194 | ``` 195 | 196 | bool flag accepts `1,t,true,yes,on` as true, `0,f,false,no,off` as false. 197 | 198 | #### Value bind 199 | 200 | You can bind a variable for a `Flag.Value`, which will be set after parsed. 201 | 202 | ```go 203 | var name string 204 | 205 | app := cli.NewApp() 206 | 207 | app.Flags = []*cli.Flag { 208 | { 209 | Name: "name", 210 | Usage: "a name of user", 211 | Value: &name, 212 | }, 213 | } 214 | 215 | app.Action = func(c *cli.Context) { 216 | fmt.Printf("Hello %v\n", name) 217 | } 218 | 219 | app.Run(os.Args) 220 | ``` 221 | 222 | `Flag.Value` can accept a `cli.Value` interface or a pointer of base type. 223 | 224 | - **base type:** 225 | - `*string` 226 | - `*bool` 227 | - `*int`, `*int8`, `*int16`, `*int32`, `*int64` 228 | - `*uint`, `*uint8`, `*uint16`, `*uint32`, `*uint64` 229 | - `*float32`, `*float64` 230 | - `*time.Time`, `*time.Duration`, `*time.Location` 231 | - `*net.IP`, `*net.IPMask`, `*net.IPNet` 232 | - `*url.URL` 233 | 234 | - **slice of base type:** 235 | - `*[]string` 236 | - `*[]int`, `*[]uint`, `*[]float64` 237 | - `*[]net.IP`, `*[]net.IPNet` 238 | - `*[]url.URL` 239 | 240 | - **cli.Value:** 241 | ```go 242 | type Value interface { 243 | String() string 244 | Set(string) error 245 | } 246 | ``` 247 | 248 | > Note: If you set `*bool` as `Flag.Value`, the `Flag.IsBool` will be automatically `true`. 249 | 250 | 251 | #### Short, Long, Alias Names 252 | 253 | You can set multiply name in a flag, a short name, a long name, or multiple alias names. 254 | 255 | ```go 256 | &cli.Flag{ 257 | Name: "o, output, output-dir", 258 | Usage: "A directory for output", 259 | } 260 | ``` 261 | 262 | Then, results in help output like: 263 | 264 | ``` 265 | -o, --output, --output-dir value A directory for output 266 | ``` 267 | 268 | #### Placeholder 269 | 270 | Sometimes it's useful to specify a flag's value within the usage string itself. 271 | 272 | For example this: 273 | 274 | ```go 275 | &cli.Flag{ 276 | Name: "o, output", 277 | Usage: "A directory for output", 278 | Placeholder: "DIR", 279 | } 280 | ``` 281 | 282 | Then, results in help output like: 283 | 284 | ``` 285 | -o DIR, --output DIR A directory for output 286 | ``` 287 | 288 | #### Default Value 289 | 290 | ```go 291 | &cli.Flag{ 292 | Name: "o, output", 293 | Usage: "A directory for output", 294 | DefValue: "/tmp/", 295 | } 296 | ``` 297 | 298 | You also can set a default value got from the Environment 299 | 300 | ```go 301 | &cli.Flag{ 302 | Name: "o, output", 303 | Usage: "A directory for output", 304 | EnvVar: "APP_OUTPUT_DIR", 305 | } 306 | ``` 307 | 308 | The `EnvVar` may also be given as a comma-delimited "cascade", 309 | where the first environment variable that resolves is used as the default. 310 | 311 | ```go 312 | EnvVar: "APP_OUTPUT,APP_OUTPUT_DIR", 313 | ``` 314 | 315 | #### NoOptDefVal 316 | 317 | If a flag has a `NoOptDefVal` and the flag is set on the command line without an option 318 | the flag will be set to the `NoOptDefVal`. 319 | 320 | For example given: 321 | 322 | ```go 323 | &cli.Flag{ 324 | Name: "flagname", 325 | DefValue: "123", 326 | NoOptDefVal: "456", 327 | Value: &val 328 | } 329 | ``` 330 | 331 | Would result in something like 332 | 333 | | Parsed Arguments | Resulting Value | 334 | | ------------- | ------------- | 335 | | --flagname=000 | val=000 | 336 | | --flagname | val=456 | 337 | | [nothing] | val=123 | 338 | 339 | #### Hidden flags 340 | 341 | It is possible to mark a flag as hidden, meaning it will still function as normal, 342 | however will not show up in usage/help text. 343 | 344 | ```go 345 | &cli.Flag{ 346 | Name: "secretFlag", 347 | Hidden: true, 348 | } 349 | ``` 350 | 351 | ### Commands 352 | 353 | Commands can be defined for a more git-like command line app. 354 | 355 | ```go 356 | package main 357 | 358 | import ( 359 | "fmt" 360 | "os" 361 | "strings" 362 | "github.com/subchen/go-cli/v3" 363 | ) 364 | 365 | func main() { 366 | app := cli.NewApp() 367 | app.Name = "git" 368 | app.Commands = []*cli.Command{ 369 | { 370 | Name: "add", 371 | Usage: "Add file contents to the index", 372 | Action: func(c *cli.Context) { 373 | fmt.Println("added files: ", strings.Join(c.Args(), ", ")) 374 | }, 375 | }, 376 | { 377 | // alias name 378 | Name: "commit, co", 379 | Usage: "Record changes to the repository", 380 | Flags: []*cli.Flag { 381 | { 382 | Name: "m, message", 383 | Usage: "commit message", 384 | }, 385 | }, 386 | Hidden: false, 387 | Action: func(c *cli.Context) { 388 | fmt.Println("commit message: ", c.GetString("m")) 389 | }, 390 | }, 391 | } 392 | 393 | app.SeeAlso = `https://github.com/subchen/go-cli 394 | https://github.com/subchen/go-cli/wiki` 395 | 396 | app.Run(os.Args) 397 | } 398 | ``` 399 | 400 | Also, you can use sub-commands in a command. 401 | 402 | ## Generate Help 403 | 404 | The default help flag (`--help`) is defined in `cli.App` and `cli.Command`. 405 | 406 | ### Customize help 407 | 408 | All of the help text generation may be customized. 409 | A help template is exposed as variable `cli.HelpTemplate`, that can be override. 410 | 411 | ```go 412 | // Append copyright 413 | cli.HelpTemplate = cli.HelpTemplate + "@2017 Your company, Inc.\n\n" 414 | ``` 415 | 416 | Or, you can rewrite a help using customized func. 417 | 418 | ```go 419 | app := cli.NewApp() 420 | 421 | app.ShowHelp = func(c *cli.HelpContext) { 422 | fmt.Println("this is my help generated.") 423 | } 424 | 425 | app.Run(os.Args) 426 | ``` 427 | 428 | ## Generate Version 429 | 430 | The default version flag (`--version`) is defined in `cli.App`. 431 | 432 | ```go 433 | app := cli.NewApp() 434 | app.Name = "hello" 435 | app.Version = "1.0.0" 436 | app.BuildInfo = &cli.BuildInfo{ 437 | GitBranch: "master", 438 | GitCommit: "320279c1a9a6537cdfd1e526063f6a748bb1fec3", 439 | GitRevCount: "1234", 440 | Timestamp: "Sat May 13 19:53:08 UTC 2017", 441 | } 442 | app.Run(os.Args) 443 | ``` 444 | 445 | Then, `./hello --version` results like: 446 | 447 | ```bash 448 | Name: hello 449 | Version: 1.0.0 450 | Patches: 1234 451 | Git branch: master 452 | Git commit: 320279c1a9a6537cdfd1e526063f6a748bb1fec3 453 | Built: Sat May 13 19:53:08 UTC 2017 454 | Go version: go1.8.1 455 | OS/Arch: darwin/amd64 456 | ``` 457 | 458 | ### Customize version 459 | 460 | You can rewrite version output using customized func. 461 | 462 | ```go 463 | app := cli.NewApp() 464 | 465 | app.ShowVersion = func(app *App) { 466 | fmt.Println("Version: ", app.Version) 467 | } 468 | 469 | app.Run(os.Args) 470 | ``` 471 | 472 | ## Error Handler 473 | 474 | ### OnCommandNotFound 475 | 476 | `go-cli` provides `OnCommandNotFound` func to handle an error if command/sub-command is not found. 477 | 478 | ```go 479 | app := cli.NewApp() 480 | app.Flags = ... 481 | app.Commands = ... 482 | 483 | app.OnCommandNotFound = func(c *cli.Context, command string) { 484 | c.ShowError(fmt.Errorf("Command not found: %s", command)) 485 | } 486 | 487 | app.Run(os.Args) 488 | ``` 489 | 490 | ### OnActionPanic 491 | 492 | `go-cli` provides `OnActionPanic` func to handle an error if panic in action. 493 | 494 | ```go 495 | app := cli.NewApp() 496 | app.Flags = ... 497 | app.Commands = ... 498 | 499 | app.OnActionPanic = func(c *cli.Context, err error) { 500 | os.Stderr.WriteString(fmt.Sprintf("fatal: %v\n", err)) 501 | } 502 | 503 | app.Run(os.Args) 504 | ``` 505 | 506 | > Notes: `go-cli` will only output error message without golang error stacks if app.OnActionPanic is nil. 507 | 508 | ## Contributing 509 | 510 | - Fork it 511 | - Create your feature branch (git checkout -b my-new-feature) 512 | - Commit your changes (git commit -am 'Add some feature') 513 | - Push to the branch (git push origin my-new-feature) 514 | - Create new Pull Request 515 | 516 | 517 | ## License 518 | 519 | Apache 2.0 license. See [LICENSE](https://github.com/subchen/go-cli/blob/master/LICENSE) 520 | 521 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // App is the main structure of a cli application 10 | type App struct { 11 | // The name of the program. Defaults to path.Base(os.Args[0]) 12 | Name string 13 | // The version of the program 14 | Version string 15 | // Short description of the program. 16 | Usage string 17 | // Text to override the USAGE section of help 18 | UsageText string 19 | // Long description of the program 20 | Description string 21 | // Authors of the program 22 | Authors string 23 | // Examples of the program 24 | Examples string 25 | // SeeAlso of the program 26 | SeeAlso string 27 | 28 | // build information, show in --version 29 | BuildInfo *BuildInfo 30 | 31 | // List of flags to parse 32 | Flags []*Flag 33 | // List of commands to execute 34 | Commands []*Command 35 | 36 | // Hidden --help and --version from usage 37 | HiddenHelp bool 38 | HiddenVersion bool 39 | 40 | // Display full help 41 | ShowHelp func(*HelpContext) 42 | // Display full version 43 | ShowVersion func(*App) 44 | 45 | // The action to execute when no subcommands are specified 46 | Action func(*Context) 47 | 48 | // Execute this function if the proper command cannot be found 49 | OnCommandNotFound func(*Context, string) 50 | 51 | // Handler if panic in app.Action() and command.Action() 52 | OnActionPanic func(*Context, error) 53 | } 54 | 55 | // NewApp creates a new cli Application 56 | func NewApp() *App { 57 | return &App{ 58 | Name: filepath.Base(os.Args[0]), 59 | Usage: "A new cli application", 60 | Version: "0.0.0", 61 | ShowHelp: showHelp, 62 | ShowVersion: showVersion, 63 | } 64 | } 65 | 66 | func (a *App) initialize() { 67 | // add --help 68 | a.Flags = append(a.Flags, &Flag{ 69 | Name: "help", 70 | Usage: "print this usage", 71 | IsBool: true, 72 | Hidden: a.HiddenHelp, 73 | }) 74 | // add --version 75 | a.Flags = append(a.Flags, &Flag{ 76 | Name: "version", 77 | Usage: "print version information", 78 | IsBool: true, 79 | Hidden: a.HiddenVersion, 80 | }) 81 | 82 | // initialize flags 83 | for _, f := range a.Flags { 84 | f.initialize() 85 | } 86 | } 87 | 88 | // Run is the entry point to the cli app, parse argument and call Execute() or command.Execute() 89 | func (a *App) Run(arguments []string) { 90 | a.initialize() 91 | 92 | // parse cli arguments 93 | cl := &commandline{ 94 | flags: a.Flags, 95 | commands: a.Commands, 96 | } 97 | err := cl.parse(arguments[1:]) 98 | 99 | // build context 100 | newCtx := &Context{ 101 | name: a.Name, 102 | app: a, 103 | flags: a.Flags, 104 | commands: a.Commands, 105 | args: cl.args, 106 | } 107 | 108 | if err != nil { 109 | newCtx.ShowError(err) 110 | } 111 | 112 | // show --help 113 | if newCtx.GetBool("help") { 114 | newCtx.ShowHelpAndExit(0) 115 | } 116 | // show --version 117 | if newCtx.GetBool("version") { 118 | a.ShowVersion(a) 119 | os.Exit(0) 120 | } 121 | 122 | // command not found 123 | if cl.command == nil && len(a.Commands) > 0 && len(cl.args) > 0 { 124 | cmd := cl.args[0] 125 | if a.OnCommandNotFound != nil { 126 | a.OnCommandNotFound(newCtx, cmd) 127 | } else { 128 | newCtx.ShowError(fmt.Errorf("no such command: %s", cmd)) 129 | } 130 | return 131 | } 132 | 133 | // run command 134 | if cl.command != nil { 135 | cl.command.Run(newCtx) 136 | return 137 | } 138 | 139 | if a.Action != nil { 140 | defer newCtx.handlePanic() 141 | a.Action(newCtx) 142 | } else { 143 | newCtx.ShowHelpAndExit(0) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /app_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAppRun(t *testing.T) { 8 | run := false 9 | app := &App{ 10 | Action: func(ctx *Context) { 11 | run = true 12 | }, 13 | } 14 | 15 | app.Run([]string{"app"}) 16 | 17 | if run == false { 18 | t.Fatal("no app run") 19 | } 20 | } 21 | 22 | func TestAppRunCmd(t *testing.T) { 23 | run := false 24 | app := &App{ 25 | Commands: []*Command{ 26 | { 27 | Name: "cmd", 28 | Action: func(ctx *Context) { 29 | run = true 30 | }, 31 | }, 32 | }, 33 | } 34 | 35 | app.Run([]string{"app", "cmd"}) 36 | 37 | if run == false { 38 | t.Fatal("no command run") 39 | } 40 | } 41 | 42 | func TestAppRunCmdNotFound(t *testing.T) { 43 | run := false 44 | app := &App{ 45 | Commands: []*Command{ 46 | { 47 | Name: "xx", 48 | }, 49 | }, 50 | OnCommandNotFound: func(*Context, string) { 51 | run = true 52 | }, 53 | } 54 | 55 | app.Run([]string{"app", "cmd", "xxx"}) 56 | 57 | if run == false { 58 | t.Fatal("OnCommandNotFound not hit") 59 | } 60 | } 61 | 62 | func TestAppRunPanic(t *testing.T) { 63 | run := false 64 | app := &App{ 65 | Action: func(ctx *Context) { 66 | panic("err") 67 | }, 68 | OnActionPanic: func(*Context, error) { 69 | run = true 70 | }, 71 | } 72 | 73 | exit = func(int) {} 74 | 75 | app.Run([]string{"app"}) 76 | 77 | if run == false { 78 | t.Fatal("OnActionPanic not hit") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /build_info.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | // BuildInfo stores app build info 8 | type BuildInfo struct { 9 | Timestamp string 10 | GitBranch string 11 | GitCommit string 12 | GitRevCount string 13 | } 14 | 15 | // ParseBuildInfo parse a buildinfo string info struct 16 | func ParseBuildInfo(info string) *BuildInfo { 17 | return &BuildInfo{ 18 | Timestamp: _readValue(info, "time"), 19 | GitBranch: _readValue(info, "branch"), 20 | GitCommit: _readValue(info, "commit"), 21 | GitRevCount: _readValue(info, "patches"), 22 | } 23 | } 24 | 25 | func _readValue(input, name string) string { 26 | re := regexp.MustCompile(`(^|\s)` + name + `:("[^"]*"|'[^']*'|[[:graph:]]+)($|\s)`) 27 | matched := re.FindAllStringSubmatch(input, 1) 28 | if matched != nil { 29 | value := matched[0][2] 30 | if value[0] == '"' || value[0] == '\'' { 31 | value = value[1 : len(value)-1] 32 | } 33 | return value 34 | } 35 | return "" 36 | } 37 | -------------------------------------------------------------------------------- /build_info_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestParseBuildInfo(t *testing.T) { 8 | input := `time:"Sat May 13 19:53:08 UTC 2017" branch:master commit:320279c patches:1234` 9 | 10 | buildinfo := ParseBuildInfo(input) 11 | 12 | if buildinfo.Timestamp != "Sat May 13 19:53:08 UTC 2017" { 13 | t.Error("parsed time is wrong") 14 | } 15 | if buildinfo.GitBranch != "master" { 16 | t.Error("parsed branch is wrong") 17 | } 18 | if buildinfo.GitCommit != "320279c" { 19 | t.Error("parsed commit is wrong") 20 | } 21 | if buildinfo.GitRevCount != "1234" { 22 | t.Error("parsed patches is wrong") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // Command is a subcommand for a cli.App 9 | type Command struct { 10 | // The name of the program. Defaults to path.Base(os.Args[0]) 11 | Name string 12 | // Short description of the program. 13 | Usage string 14 | // Text to override the USAGE section of help 15 | UsageText string 16 | // Long description of the program 17 | Description string 18 | // Examples of the program 19 | Examples string 20 | // SeeAlso of the program 21 | SeeAlso string 22 | 23 | // List of flags to parse 24 | Flags []*Flag 25 | // List of commands to execute 26 | Commands []*Command 27 | 28 | // hidden --help from usage 29 | HiddenHelp bool 30 | 31 | // Treat all flags as normal arguments if true 32 | SkipFlagParsing bool 33 | 34 | // Boolean to hide this command from help 35 | Hidden bool 36 | 37 | // Display full help 38 | ShowHelp func(*HelpContext) 39 | 40 | // The action to execute when no subcommands are specified 41 | Action func(*Context) 42 | 43 | // Execute this function if the proper command cannot be found 44 | OnCommandNotFound func(*Context, string) 45 | } 46 | 47 | func (c *Command) initialize() { 48 | // add --help 49 | c.Flags = append(c.Flags, &Flag{ 50 | Name: "help", 51 | Usage: "print this usage", 52 | IsBool: true, 53 | Hidden: c.HiddenHelp, 54 | }) 55 | 56 | // initialize flags 57 | for _, f := range c.Flags { 58 | f.initialize() 59 | } 60 | } 61 | 62 | // Run is the entry point to the command, parse argument and call Execute() or subcommand.Execute() 63 | func (c *Command) Run(ctx *Context) { 64 | c.initialize() 65 | 66 | if c.ShowHelp == nil { 67 | c.ShowHelp = showHelp 68 | } 69 | 70 | // parse cli arguments 71 | cl := &commandline{ 72 | flags: c.Flags, 73 | commands: c.Commands, 74 | } 75 | var err error 76 | if c.SkipFlagParsing { 77 | cl.args = ctx.args[1:] 78 | } else { 79 | err = cl.parse(ctx.args[1:]) 80 | } 81 | 82 | // build context 83 | newCtx := &Context{ 84 | name: ctx.name + " " + c.Name, 85 | app: ctx.app, 86 | command: c, 87 | flags: c.Flags, 88 | commands: c.Commands, 89 | args: cl.args, 90 | parent: ctx, 91 | } 92 | 93 | if err != nil { 94 | newCtx.ShowError(err) 95 | } 96 | 97 | // show --help 98 | if newCtx.GetBool("help") { 99 | newCtx.ShowHelpAndExit(0) 100 | } 101 | 102 | // command not found 103 | if cl.command == nil && len(c.Commands) > 0 && len(cl.args) > 0 { 104 | cmd := cl.args[0] 105 | if c.OnCommandNotFound != nil { 106 | c.OnCommandNotFound(newCtx, cmd) 107 | } else { 108 | newCtx.ShowError(fmt.Errorf("no such command: %s", cmd)) 109 | } 110 | return 111 | } 112 | 113 | // run command 114 | if cl.command != nil { 115 | cl.command.Run(newCtx) 116 | return 117 | } 118 | 119 | if c.Action != nil { 120 | defer newCtx.handlePanic() 121 | c.Action(newCtx) 122 | } else { 123 | newCtx.ShowHelpAndExit(0) 124 | } 125 | } 126 | 127 | // Names returns the names including short names and aliases 128 | func (c *Command) Names() []string { 129 | names := strings.Split(c.Name, ",") 130 | for i, name := range names { 131 | names[i] = strings.TrimSpace(name) 132 | } 133 | return names 134 | } 135 | 136 | func lookupCommand(commands []*Command, name string) *Command { 137 | for _, c := range commands { 138 | for _, n := range c.Names() { 139 | if n == name { 140 | return c 141 | } 142 | } 143 | } 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /command_line_parser.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type commandline struct { 9 | // defined 10 | flags []*Flag 11 | commands []*Command 12 | 13 | // parsed results 14 | command *Command 15 | args []string 16 | } 17 | 18 | func (c *commandline) parse(arguments []string) (err error) { 19 | for i := 0; i < len(arguments); i++ { 20 | arg := arguments[i] 21 | if arg == "--" { 22 | c.args = append(c.args, arguments[i+1:]...) 23 | break 24 | } else if strings.HasPrefix(arg, "-") && arg != "-" { 25 | peekedNext, err := c.parseOneArg(i, arguments) 26 | if err != nil { 27 | return err 28 | } 29 | if peekedNext { 30 | i++ 31 | } 32 | } else { 33 | if len(c.commands) == 0 { 34 | c.args = append(c.args, arg) 35 | } else { 36 | c.command = lookupCommand(c.commands, arg) 37 | c.args = append(c.args, arguments[i:]...) 38 | break 39 | } 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func (c *commandline) parseOneArg(i int, arguments []string) (bool, error) { 47 | prefix := "" 48 | name := "" 49 | valueInline := "" 50 | valueNext := "" 51 | 52 | arg := arguments[i] 53 | if strings.HasPrefix(arg, "--") { // long flag 54 | prefix = "--" 55 | kv := strings.SplitN(arg[2:], "=", 2) 56 | name = kv[0] 57 | if len(kv) == 2 { // --name=value 58 | valueInline = kv[1] 59 | } else if i+1 < len(arguments) { // --name value 60 | next := arguments[i+1] 61 | if !strings.HasPrefix(next, "-") { 62 | valueNext = next 63 | } 64 | } 65 | } else { // short flag 66 | prefix = "-" 67 | name = arg[1:2] 68 | if len(arg) > 2 { 69 | if arg[2] == '=' { 70 | valueInline = arg[3:] // -x=value 71 | } else { 72 | valueInline = arg[2:] // -xvalue 73 | } 74 | } else if i+1 < len(arguments) { // -x value 75 | next := arguments[i+1] 76 | if !strings.HasPrefix(next, "-") { 77 | valueNext = next 78 | } 79 | } 80 | } 81 | 82 | if len(valueInline) > 0 { 83 | // remove quote for inline value 84 | if strings.HasPrefix(valueInline, "\"") && strings.HasSuffix(valueInline, "\"") { 85 | valueInline = valueInline[1 : len(valueInline)-1] 86 | } else if strings.HasPrefix(valueInline, "'") && strings.HasSuffix(valueInline, "'") { 87 | valueInline = valueInline[1 : len(valueInline)-1] 88 | } 89 | } 90 | 91 | flag := lookupFlag(c.flags, name) 92 | if flag == nil { 93 | return false, fmt.Errorf("unrecognized option '%s'", prefix+name) 94 | } 95 | 96 | if flag.IsBool { 97 | if valueInline == "" { 98 | valueInline = "true" 99 | } 100 | err := flag.SetValue(valueInline) 101 | return false, err 102 | } 103 | 104 | value := valueInline + valueNext 105 | if value == "" { 106 | value = flag.NoOptDefValue 107 | } 108 | if value == "" { 109 | return false, fmt.Errorf("option requires an argument '%s'", prefix+name) 110 | } 111 | 112 | err := flag.SetValue(value) 113 | if err != nil { 114 | return false, err 115 | } 116 | 117 | return valueNext != "", nil 118 | } 119 | -------------------------------------------------------------------------------- /command_line_parser_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCommandlineParseBoolFlag(t *testing.T) { 8 | var b1, b2, b3, b4 bool 9 | 10 | cl := &commandline{ 11 | flags: []*Flag{ 12 | {Name: "b1", Value: &b1}, 13 | {Name: "b2", Value: &b2}, 14 | {Name: "b3", Value: &b3}, 15 | {Name: "b4", Value: &b4, DefValue: "true"}, 16 | }, 17 | } 18 | 19 | // initialize flags 20 | for _, f := range cl.flags { 21 | f.initialize() 22 | } 23 | 24 | args := []string{ 25 | "--b1", 26 | "--b2=false", 27 | "--b3=on", 28 | } 29 | 30 | err := cl.parse(args) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | 35 | if b1 != true { 36 | t.Error("b1 != true") 37 | } 38 | if b2 != false { 39 | t.Error("b2 != false") 40 | } 41 | if b3 != true { 42 | t.Error("b3 != true") 43 | } 44 | if b4 != true { 45 | t.Error("b4 != true") 46 | } 47 | } 48 | 49 | func TestCommandlineParseStringFlag(t *testing.T) { 50 | var a, b, c string 51 | var s1, s2, s3, s4 string 52 | 53 | cl := &commandline{ 54 | flags: []*Flag{ 55 | {Name: "s1", Value: &s1}, 56 | {Name: "s2", Value: &s2}, 57 | {Name: "s3", Value: &s3}, 58 | {Name: "s4", Value: &s4}, 59 | {Name: "a", Value: &a}, 60 | {Name: "b", Value: &b}, 61 | {Name: "c", Value: &c}, 62 | }, 63 | } 64 | 65 | // initialize flags 66 | for _, f := range cl.flags { 67 | f.initialize() 68 | } 69 | 70 | args := []string{ 71 | "--s1=val", 72 | `--s2="val"`, 73 | `--s3='val'`, 74 | "--s4", "val", 75 | 76 | "-a", "val", 77 | "-b=val", 78 | "-cval", 79 | } 80 | 81 | err := cl.parse(args) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | if a != "val" { 87 | t.Error("wrong: -x arg") 88 | } 89 | if b != "val" { 90 | t.Error("wrong -x=arg") 91 | } 92 | if c != "val" { 93 | t.Error("wrong -xarg") 94 | } 95 | 96 | if s1 != "val" { 97 | t.Error("wrong: --long=arg") 98 | } 99 | if s2 != "val" { 100 | t.Error(`wrong --long="arg"`) 101 | } 102 | if s3 != "val" { 103 | t.Error(`wrong --long='arg'`) 104 | } 105 | if s4 != "val" { 106 | t.Error("wrong: --long arg") 107 | } 108 | } 109 | 110 | func TestCommandlineParseRawFlag(t *testing.T) { 111 | cl := &commandline{} 112 | 113 | args := []string{"--", "-a", "--bb"} 114 | 115 | err := cl.parse(args) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | if len(cl.args) != 2 { 121 | t.Error("wrong parse raw: -- arg ...") 122 | } 123 | } 124 | 125 | func TestCommandlineParseFlagNotFound(t *testing.T) { 126 | cl := &commandline{} 127 | 128 | args := []string{"--test"} 129 | 130 | err := cl.parse(args) 131 | if err == nil { 132 | t.Fatal("no error found") 133 | } 134 | if err.Error() != `unrecognized option '--test'` { 135 | t.Fatalf("unexpected error: %v", err) 136 | } 137 | } 138 | 139 | func TestCommandlineParseCommand(t *testing.T) { 140 | cl := &commandline{ 141 | commands: []*Command{ 142 | { 143 | Name: "debug", 144 | }, 145 | }, 146 | } 147 | 148 | args := []string{"debug", "-a", "--bb"} 149 | 150 | err := cl.parse(args) 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | 155 | if cl.command == nil { 156 | t.Fatal("no command found") 157 | } 158 | if cl.command.Name != "debug" { 159 | t.Fatal("wrong parsed command") 160 | } 161 | } 162 | 163 | func TestCommandlineParseCommandNotFound(t *testing.T) { 164 | cl := &commandline{ 165 | commands: []*Command{ 166 | { 167 | Name: "debug", 168 | }, 169 | }, 170 | } 171 | 172 | args := []string{"help"} 173 | 174 | err := cl.parse(args) 175 | if err != nil { 176 | t.Fatal(err) 177 | } 178 | if cl.command != nil { 179 | t.Fatal("command should be not found") 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /command_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCommandRun(t *testing.T) { 8 | ctx := &Context{ 9 | args: []string{"cmd"}, 10 | } 11 | 12 | run := false 13 | c := &Command{ 14 | Action: func(ctx *Context) { 15 | run = true 16 | }, 17 | } 18 | 19 | c.Run(ctx) 20 | 21 | if run == false { 22 | t.Fatal("no command run") 23 | } 24 | } 25 | 26 | func TestCommandRunSubCmd(t *testing.T) { 27 | ctx := &Context{ 28 | args: []string{"cmd", "subcmd"}, 29 | } 30 | 31 | run := false 32 | c := &Command{ 33 | Commands: []*Command{ 34 | { 35 | Name: "subcmd", 36 | Action: func(ctx *Context) { 37 | run = true 38 | }, 39 | }, 40 | }, 41 | } 42 | 43 | c.Run(ctx) 44 | if run == false { 45 | t.Fatal("no sub command run") 46 | } 47 | } 48 | 49 | func TestCommandRunSubCmdNotFound(t *testing.T) { 50 | ctx := &Context{ 51 | args: []string{"cmd", "subcmd", "xxxxx"}, 52 | } 53 | 54 | run := false 55 | c := &Command{ 56 | Commands: []*Command{ 57 | { 58 | Name: "xx", 59 | }, 60 | }, 61 | OnCommandNotFound: func(*Context, string) { 62 | run = true 63 | }, 64 | } 65 | 66 | c.Run(ctx) 67 | if run == false { 68 | t.Fatal("OnCommandNotFound not hit") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // exit variable for tesing hook 11 | var exit = os.Exit 12 | 13 | // Context is a type that is passed through to 14 | // each Handler action in a cli application. Context 15 | // can be used to retrieve context-specific Args and 16 | // parsed command-line options. 17 | type Context struct { 18 | name string 19 | app *App 20 | command *Command 21 | flags []*Flag 22 | commands []*Command 23 | args []string 24 | parent *Context 25 | } 26 | 27 | // Name returns app/command full name 28 | func (c *Context) Name() string { 29 | return c.name 30 | } 31 | 32 | // Parent returns parent context if exists 33 | func (c *Context) Parent() *Context { 34 | return c.parent 35 | } 36 | 37 | // Global returns top context if exists 38 | func (c *Context) Global() *Context { 39 | ctx := c 40 | for { 41 | if ctx.parent == nil { 42 | return ctx 43 | } 44 | ctx = ctx.parent 45 | } 46 | } 47 | 48 | // IsSet returns flag is visited in cli args 49 | func (c *Context) IsSet(name string) bool { 50 | f := lookupFlag(c.flags, name) 51 | if f != nil { 52 | return f.visited 53 | } 54 | return false 55 | } 56 | 57 | // GetString returns flag value as string 58 | func (c *Context) GetString(name string) string { 59 | f := lookupFlag(c.flags, name) 60 | if f != nil { 61 | return f.GetValue() 62 | } 63 | return "" 64 | } 65 | 66 | // GetStringSlice returns flag value as string slice 67 | func (c *Context) GetStringSlice(name string) []string { 68 | f := lookupFlag(c.flags, name) 69 | if f != nil { 70 | return strings.Split(f.GetValue(), ",") 71 | } 72 | return nil 73 | } 74 | 75 | // GetBool returns flag value as bool 76 | func (c *Context) GetBool(name string) bool { 77 | f := lookupFlag(c.flags, name) 78 | if f != nil { 79 | b, err := strconv.ParseBool(f.GetValue()) 80 | if err == nil { 81 | return b 82 | } 83 | } 84 | return false 85 | } 86 | 87 | // GetInt returns flag value as int 88 | func (c *Context) GetInt(name string) int { 89 | f := lookupFlag(c.flags, name) 90 | if f != nil { 91 | v, err := strconv.ParseInt(f.GetValue(), 0, 0) 92 | if err == nil { 93 | return int(v) 94 | } 95 | } 96 | return 0 97 | } 98 | 99 | // GetInt8 returns flag value as int8 100 | func (c *Context) GetInt8(name string) int8 { 101 | f := lookupFlag(c.flags, name) 102 | if f != nil { 103 | v, err := strconv.ParseInt(f.GetValue(), 0, 8) 104 | if err == nil { 105 | return int8(v) 106 | } 107 | } 108 | return 0 109 | } 110 | 111 | // GetInt16 returns flag value as int16 112 | func (c *Context) GetInt16(name string) int16 { 113 | f := lookupFlag(c.flags, name) 114 | if f != nil { 115 | v, err := strconv.ParseInt(f.GetValue(), 0, 16) 116 | if err == nil { 117 | return int16(v) 118 | } 119 | } 120 | return 0 121 | } 122 | 123 | // GetInt32 returns flag value as int32 124 | func (c *Context) GetInt32(name string) int32 { 125 | f := lookupFlag(c.flags, name) 126 | if f != nil { 127 | v, err := strconv.ParseInt(f.GetValue(), 0, 32) 128 | if err == nil { 129 | return int32(v) 130 | } 131 | } 132 | return 0 133 | } 134 | 135 | // GetInt64 returns flag value as int64 136 | func (c *Context) GetInt64(name string) int64 { 137 | f := lookupFlag(c.flags, name) 138 | if f != nil { 139 | v, err := strconv.ParseInt(f.GetValue(), 0, 64) 140 | if err == nil { 141 | return int64(v) 142 | } 143 | } 144 | return 0 145 | } 146 | 147 | // GetUint returns flag value as uint 148 | func (c *Context) GetUint(name string) uint { 149 | f := lookupFlag(c.flags, name) 150 | if f != nil { 151 | v, err := strconv.ParseUint(f.GetValue(), 0, 0) 152 | if err == nil { 153 | return uint(v) 154 | } 155 | } 156 | return 0 157 | } 158 | 159 | // GetUint8 returns flag value as uint8 160 | func (c *Context) GetUint8(name string) uint8 { 161 | f := lookupFlag(c.flags, name) 162 | if f != nil { 163 | v, err := strconv.ParseUint(f.GetValue(), 0, 8) 164 | if err == nil { 165 | return uint8(v) 166 | } 167 | } 168 | return 0 169 | } 170 | 171 | // GetUint16 returns flag value as uint16 172 | func (c *Context) GetUint16(name string) uint16 { 173 | f := lookupFlag(c.flags, name) 174 | if f != nil { 175 | v, err := strconv.ParseUint(f.GetValue(), 0, 16) 176 | if err == nil { 177 | return uint16(v) 178 | } 179 | } 180 | return 0 181 | } 182 | 183 | // GetUint32 returns flag value as uint32 184 | func (c *Context) GetUint32(name string) uint32 { 185 | f := lookupFlag(c.flags, name) 186 | if f != nil { 187 | v, err := strconv.ParseUint(f.GetValue(), 0, 32) 188 | if err == nil { 189 | return uint32(v) 190 | } 191 | } 192 | return 0 193 | } 194 | 195 | // GetUint64 returns flag value as uint64 196 | func (c *Context) GetUint64(name string) uint64 { 197 | f := lookupFlag(c.flags, name) 198 | if f != nil { 199 | v, err := strconv.ParseUint(f.GetValue(), 0, 64) 200 | if err == nil { 201 | return uint64(v) 202 | } 203 | } 204 | return 0 205 | } 206 | 207 | // GetFloat32 returns flag value as float32 208 | func (c *Context) GetFloat32(name string) float32 { 209 | f := lookupFlag(c.flags, name) 210 | if f != nil { 211 | v, err := strconv.ParseFloat(f.GetValue(), 32) 212 | if err == nil { 213 | return float32(v) 214 | } 215 | } 216 | return 0 217 | } 218 | 219 | // GetFloat64 returns flag value as float64 220 | func (c *Context) GetFloat64(name string) float64 { 221 | f := lookupFlag(c.flags, name) 222 | if f != nil { 223 | v, err := strconv.ParseFloat(f.GetValue(), 64) 224 | if err == nil { 225 | return float64(v) 226 | } 227 | } 228 | return 0 229 | } 230 | 231 | // NArg returns number of non-flag arguments 232 | func (c *Context) NArg() int { 233 | return len(c.args) 234 | } 235 | 236 | // Arg returns the i'th non-flag argument 237 | func (c *Context) Arg(n int) string { 238 | return c.args[n] 239 | } 240 | 241 | // Args returns the non-flag arguments. 242 | func (c *Context) Args() []string { 243 | return c.args 244 | } 245 | 246 | // ShowHelp shows help and 247 | func (c *Context) ShowHelp() { 248 | if c.command != nil { 249 | c.command.ShowHelp(newCommandHelpContext(c.name, c.command, c.app)) 250 | } else { 251 | c.app.ShowHelp(newAppHelpContext(c.name, c.app)) 252 | } 253 | } 254 | 255 | // ShowHelpAndExit shows help and exit 256 | func (c *Context) ShowHelpAndExit(code int) { 257 | c.ShowHelp() 258 | exit(code) 259 | } 260 | 261 | // ShowError shows error and exit(1) 262 | func (c *Context) ShowError(err error) { 263 | w := os.Stderr 264 | fmt.Fprintln(w, err) 265 | fmt.Fprintln(w, fmt.Sprintf("\nRun '%s --help' for more information", c.name)) 266 | exit(1) 267 | } 268 | 269 | func (c *Context) handlePanic() { 270 | if e := recover(); e != nil { 271 | if c.app.OnActionPanic != nil { 272 | err, ok := e.(error) 273 | if !ok { 274 | err = fmt.Errorf("%v", e) 275 | } 276 | c.app.OnActionPanic(c, err) 277 | } else { 278 | os.Stderr.WriteString(fmt.Sprintf("fatal: %v\n", e)) 279 | } 280 | exit(1) 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestContextGet(t *testing.T) { 9 | c := &Context{ 10 | flags: []*Flag{ 11 | {Name: "f1"}, 12 | {Name: "f2", Value: new(bool)}, 13 | {Name: "f3", Value: new([]string)}, 14 | {Name: "f4"}, 15 | }, 16 | } 17 | 18 | // initialize flags 19 | for _, f := range c.flags { 20 | f.initialize() 21 | } 22 | 23 | lookupFlag(c.flags, "f1").SetValue("123") 24 | lookupFlag(c.flags, "f2").SetValue("true") 25 | lookupFlag(c.flags, "f3").SetValue("a") 26 | lookupFlag(c.flags, "f3").SetValue("b") 27 | 28 | // IsSet 29 | if !c.IsSet("f1") { 30 | t.Error("f1 is not visited") 31 | } 32 | if c.IsSet("f4") { 33 | t.Error("f4 is visited") 34 | } 35 | 36 | // GetXXX 37 | if c.GetString("f1") != "123" { 38 | t.Error("f1 GetString is wrong") 39 | } 40 | if c.GetInt("f1") != 123 { 41 | t.Error("f1 GetInt is wrong") 42 | } 43 | if c.GetInt8("f1") != 123 { 44 | t.Error("f1 GetInt8 is wrong") 45 | } 46 | if c.GetInt16("f1") != 123 { 47 | t.Error("f1 GetInt16 is wrong") 48 | } 49 | if c.GetInt32("f1") != 123 { 50 | t.Error("f1 GetInt32 is wrong") 51 | } 52 | if c.GetInt64("f1") != 123 { 53 | t.Error("f1 GetInt64 is wrong") 54 | } 55 | if c.GetUint("f1") != 123 { 56 | t.Error("f1 GetUint is wrong") 57 | } 58 | if c.GetUint8("f1") != 123 { 59 | t.Error("f1 GetUint8 is wrong") 60 | } 61 | if c.GetUint16("f1") != 123 { 62 | t.Error("f1 GetUint16 is wrong") 63 | } 64 | if c.GetUint32("f1") != 123 { 65 | t.Error("f1 GetUint32 is wrong") 66 | } 67 | if c.GetUint64("f1") != 123 { 68 | t.Error("f1 GetUint64 is wrong") 69 | } 70 | if c.GetFloat32("f1") != 123 { 71 | t.Error("f1 GetFloat32 is wrong") 72 | } 73 | if c.GetFloat64("f1") != 123 { 74 | t.Error("f1 GetFloat64 is wrong") 75 | } 76 | 77 | // GetBool 78 | if c.GetBool("f2") != true { 79 | t.Error("f2 GetBool is wrong") 80 | } 81 | 82 | // GetStringSlice 83 | if got := c.GetStringSlice("f3"); !reflect.DeepEqual(got, []string{"a", "b"}) { 84 | t.Errorf("f3 GetStringSlice is wrong, got: %v", got) 85 | } 86 | } 87 | 88 | func TestContextArg(t *testing.T) { 89 | c := &Context{ 90 | args: []string{"a", "b", "c"}, 91 | } 92 | 93 | if c.NArg() != 3 { 94 | t.Error("NArg() != 3") 95 | } 96 | if c.Arg(0) != "a" { 97 | t.Error("Arg(0) != 'a'") 98 | } 99 | if !reflect.DeepEqual(c.Args(), c.args) { 100 | t.Error("Args() is wrong") 101 | } 102 | } 103 | 104 | func TestContextParent(t *testing.T) { 105 | p := &Context{name: "p"} 106 | c := &Context{parent: p} 107 | 108 | if c.Parent().Name() != "p" { 109 | t.Error("Parent() is wrong") 110 | } 111 | if c.Global().Name() != "p" { 112 | t.Error("Global() is wrong") 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /flag.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/url" 7 | "os" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // Flag represents the state of a flag 13 | type Flag struct { 14 | Name string // name as it appears on command line 15 | Usage string // help message 16 | Placeholder string // placeholder in usage 17 | Hidden bool // allow flags to be hidden from help/usage text 18 | 19 | IsBool bool // if the flag is bool value 20 | DefValue string // default value (as text); for usage message 21 | NoOptDefValue string // default value (as text); if the flag is on the command line without any options 22 | EnvVar string // default value load from environ 23 | 24 | Value interface{} // returns final value 25 | 26 | wrapValue Value // returns final value, wrapped Flag.Value 27 | visited bool // If the user set the value 28 | } 29 | 30 | // Value is the interface to the dynamic value stored in a flag. 31 | // (The default value is represented as a string.) 32 | type Value interface { 33 | String() string 34 | Set(string) error 35 | } 36 | 37 | func (f *Flag) initialize() { 38 | if f.Value != nil { 39 | switch val := f.Value.(type) { 40 | case *bool: 41 | f.IsBool = true 42 | f.wrapValue = &boolValue{val} 43 | case *string: 44 | f.wrapValue = &stringValue{val} 45 | case *[]string: 46 | f.wrapValue = &stringSliceValue{val} 47 | case *int: 48 | f.wrapValue = &intValue{val} 49 | case *[]int: 50 | f.wrapValue = &intSliceValue{val} 51 | case *int8: 52 | f.wrapValue = &int8Value{val} 53 | case *int16: 54 | f.wrapValue = &int16Value{val} 55 | case *int32: 56 | f.wrapValue = &int32Value{val} 57 | case *int64: 58 | f.wrapValue = &int64Value{val} 59 | case *uint: 60 | f.wrapValue = &uintValue{val} 61 | case *[]uint: 62 | f.wrapValue = &uintSliceValue{val} 63 | case *uint8: 64 | f.wrapValue = &uint8Value{val} 65 | case *uint16: 66 | f.wrapValue = &uint16Value{val} 67 | case *uint32: 68 | f.wrapValue = &uint32Value{val} 69 | case *uint64: 70 | f.wrapValue = &uint64Value{val} 71 | case *float32: 72 | f.wrapValue = &float32Value{val} 73 | case *float64: 74 | f.wrapValue = &float64Value{val} 75 | case *[]float64: 76 | f.wrapValue = &float64SliceValue{val} 77 | case *time.Time: 78 | f.wrapValue = &timeValue{val} 79 | case *time.Duration: 80 | f.wrapValue = &timeDurationValue{val} 81 | case *time.Location: 82 | f.wrapValue = &timeLocationValue{val} 83 | case *net.IP: 84 | f.wrapValue = &ipValue{val} 85 | case *[]net.IP: 86 | f.wrapValue = &ipSliceValue{val} 87 | case *net.IPMask: 88 | f.wrapValue = &ipMaskValue{val} 89 | case *net.IPNet: 90 | f.wrapValue = &ipNetValue{val} 91 | case *[]net.IPNet: 92 | f.wrapValue = &ipNetSliceValue{val} 93 | case *url.URL: 94 | f.wrapValue = &urlValue{val} 95 | case *[]url.URL: 96 | f.wrapValue = &urlSliceValue{val} 97 | case Value: 98 | f.wrapValue = val 99 | default: 100 | panic(fmt.Sprintf("unknown type of flag.Value: %T", f.Value)) 101 | } 102 | } 103 | 104 | if f.Value == nil { 105 | if f.IsBool { 106 | f.wrapValue = &boolValue{new(bool)} 107 | } else { 108 | f.wrapValue = &stringValue{new(string)} 109 | } 110 | } 111 | 112 | if f.Placeholder == "" { 113 | f.Placeholder = "value" 114 | } 115 | 116 | envSet := false 117 | for _, name := range strings.Split(f.EnvVar, ",") { 118 | name = strings.TrimSpace(name) 119 | if value, ok := os.LookupEnv(name); ok { 120 | f.wrapValue.Set(value) 121 | envSet = true 122 | break 123 | } 124 | } 125 | 126 | if !envSet && f.DefValue != "" { 127 | f.wrapValue.Set(f.DefValue) 128 | } 129 | 130 | f.visited = false // reset 131 | } 132 | 133 | // Names returns the names including short names and aliases 134 | func (f *Flag) Names() []string { 135 | names := strings.Split(f.Name, ",") 136 | for i, name := range names { 137 | names[i] = strings.TrimSpace(name) 138 | } 139 | return names 140 | } 141 | 142 | // SetValue sets the value of the named flag 143 | func (f *Flag) SetValue(value string) error { 144 | f.visited = true 145 | return f.wrapValue.Set(value) 146 | } 147 | 148 | // GetValue returns the string value of flag 149 | func (f *Flag) GetValue() string { 150 | return f.wrapValue.String() 151 | } 152 | 153 | func lookupFlag(flags []*Flag, name string) *Flag { 154 | for _, f := range flags { 155 | for _, n := range f.Names() { 156 | if n == name { 157 | return f 158 | } 159 | } 160 | } 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /flag_bool.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type boolValue struct { 8 | val *bool 9 | } 10 | 11 | func (v *boolValue) Set(value string) error { 12 | switch value { 13 | case "1", "t", "true", "on", "yes", "y": 14 | *v.val = true 15 | case "0", "f", "false", "off", "no", "n": 16 | *v.val = false 17 | default: 18 | b, err := strconv.ParseBool(value) 19 | if err != nil { 20 | return err 21 | } 22 | *v.val = b 23 | } 24 | return nil 25 | } 26 | 27 | func (v *boolValue) String() string { 28 | return strconv.FormatBool(*v.val) 29 | } 30 | -------------------------------------------------------------------------------- /flag_float32.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type float32Value struct { 8 | val *float32 9 | } 10 | 11 | func (v *float32Value) Set(value string) error { 12 | val, err := strconv.ParseFloat(value, 32) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = float32(val) 18 | return nil 19 | } 20 | 21 | func (v *float32Value) String() string { 22 | return strconv.FormatFloat(float64(*v.val), 'f', -1, 32) 23 | } 24 | -------------------------------------------------------------------------------- /flag_float64.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type float64Value struct { 8 | val *float64 9 | } 10 | 11 | func (v *float64Value) Set(value string) error { 12 | val, err := strconv.ParseFloat(value, 64) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = float64(val) 18 | return nil 19 | } 20 | 21 | func (v *float64Value) String() string { 22 | return strconv.FormatFloat(float64(*v.val), 'f', -1, 64) 23 | } 24 | -------------------------------------------------------------------------------- /flag_float64_slice.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type float64SliceValue struct { 9 | val *[]float64 10 | } 11 | 12 | func (v *float64SliceValue) Set(value string) error { 13 | val, err := strconv.ParseFloat(value, 64) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | *v.val = append(*v.val, float64(val)) 19 | return nil 20 | } 21 | 22 | func (v *float64SliceValue) String() string { 23 | l := len(*v.val) 24 | if l == 0 { 25 | return "" 26 | } 27 | 28 | slice := make([]string, 0, l) 29 | for _, val := range *v.val { 30 | s := strconv.FormatFloat(float64(val), 'f', -1, 64) 31 | slice = append(slice, s) 32 | } 33 | return strings.Join(slice, ",") 34 | } 35 | -------------------------------------------------------------------------------- /flag_int.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type intValue struct { 8 | val *int 9 | } 10 | 11 | func (v *intValue) Set(value string) error { 12 | val, err := strconv.ParseInt(value, 0, 0) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = int(val) 18 | return nil 19 | } 20 | 21 | func (v *intValue) String() string { 22 | return strconv.FormatInt(int64(*v.val), 10) 23 | } 24 | -------------------------------------------------------------------------------- /flag_int16.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type int16Value struct { 8 | val *int16 9 | } 10 | 11 | func (v *int16Value) Set(value string) error { 12 | val, err := strconv.ParseInt(value, 0, 16) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = int16(val) 18 | return nil 19 | } 20 | 21 | func (v *int16Value) String() string { 22 | return strconv.FormatInt(int64(*v.val), 10) 23 | } 24 | -------------------------------------------------------------------------------- /flag_int32.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type int32Value struct { 8 | val *int32 9 | } 10 | 11 | func (v *int32Value) Set(value string) error { 12 | val, err := strconv.ParseInt(value, 0, 32) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = int32(val) 18 | return nil 19 | } 20 | 21 | func (v *int32Value) String() string { 22 | return strconv.FormatInt(int64(*v.val), 10) 23 | } 24 | -------------------------------------------------------------------------------- /flag_int64.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type int64Value struct { 8 | val *int64 9 | } 10 | 11 | func (v *int64Value) Set(value string) error { 12 | val, err := strconv.ParseInt(value, 0, 64) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = int64(val) 18 | return nil 19 | } 20 | 21 | func (v *int64Value) String() string { 22 | return strconv.FormatInt(int64(*v.val), 10) 23 | } 24 | -------------------------------------------------------------------------------- /flag_int8.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type int8Value struct { 8 | val *int8 9 | } 10 | 11 | func (v *int8Value) Set(value string) error { 12 | val, err := strconv.ParseInt(value, 0, 8) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = int8(val) 18 | return nil 19 | } 20 | 21 | func (v *int8Value) String() string { 22 | return strconv.FormatInt(int64(*v.val), 10) 23 | } 24 | -------------------------------------------------------------------------------- /flag_int_slice.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type intSliceValue struct { 9 | val *[]int 10 | } 11 | 12 | func (v *intSliceValue) Set(value string) error { 13 | val, err := strconv.ParseInt(value, 0, 0) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | *v.val = append(*v.val, int(val)) 19 | return nil 20 | } 21 | 22 | func (v *intSliceValue) String() string { 23 | l := len(*v.val) 24 | if l == 0 { 25 | return "" 26 | } 27 | 28 | slice := make([]string, 0, l) 29 | for _, val := range *v.val { 30 | s := strconv.FormatInt(int64(val), 10) 31 | slice = append(slice, s) 32 | } 33 | return strings.Join(slice, ",") 34 | } 35 | -------------------------------------------------------------------------------- /flag_ip.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type ipValue struct { 9 | val *net.IP 10 | } 11 | 12 | func (v *ipValue) Set(value string) error { 13 | val := net.ParseIP(value) 14 | if val == nil { 15 | return fmt.Errorf("invalid ip: " + value) 16 | } 17 | 18 | *v.val = val 19 | return nil 20 | } 21 | 22 | func (v *ipValue) String() string { 23 | return (*v.val).String() 24 | } 25 | -------------------------------------------------------------------------------- /flag_ip_mask.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type ipMaskValue struct { 9 | val *net.IPMask 10 | } 11 | 12 | func (v *ipMaskValue) Set(value string) error { 13 | val := parseIPv4Mask(value) 14 | if val == nil { 15 | return fmt.Errorf("invalid ip mask: " + value) 16 | } 17 | 18 | *v.val = val 19 | return nil 20 | } 21 | 22 | func (v *ipMaskValue) String() string { 23 | return (*v.val).String() 24 | } 25 | 26 | // parseIPv4Mask written in IP form (e.g. 255.255.255.0). 27 | func parseIPv4Mask(s string) net.IPMask { 28 | mask := net.ParseIP(s) 29 | if mask != nil { 30 | return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]) 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /flag_ip_net.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type ipNetValue struct { 8 | val *net.IPNet 9 | } 10 | 11 | func (v *ipNetValue) Set(value string) error { 12 | _, val, err := net.ParseCIDR(value) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = *val 18 | return nil 19 | } 20 | 21 | func (v *ipNetValue) String() string { 22 | return (*v.val).String() 23 | } 24 | -------------------------------------------------------------------------------- /flag_ip_net_slice.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | ) 7 | 8 | type ipNetSliceValue struct { 9 | val *[]net.IPNet 10 | } 11 | 12 | func (v *ipNetSliceValue) Set(value string) error { 13 | _, val, err := net.ParseCIDR(value) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | *v.val = append(*v.val, *val) 19 | return nil 20 | } 21 | 22 | func (v *ipNetSliceValue) String() string { 23 | l := len(*v.val) 24 | if l == 0 { 25 | return "" 26 | } 27 | 28 | slice := make([]string, 0, l) 29 | for _, val := range *v.val { 30 | slice = append(slice, val.String()) 31 | } 32 | return strings.Join(slice, ",") 33 | } 34 | -------------------------------------------------------------------------------- /flag_ip_slice.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | type ipSliceValue struct { 10 | val *[]net.IP 11 | } 12 | 13 | func (v *ipSliceValue) Set(value string) error { 14 | val := net.ParseIP(value) 15 | if val == nil { 16 | return fmt.Errorf("invalid ip: " + value) 17 | } 18 | 19 | *v.val = append(*v.val, val) 20 | return nil 21 | } 22 | 23 | func (v *ipSliceValue) String() string { 24 | l := len(*v.val) 25 | if l == 0 { 26 | return "" 27 | } 28 | 29 | slice := make([]string, 0, l) 30 | for _, val := range *v.val { 31 | slice = append(slice, val.String()) 32 | } 33 | return strings.Join(slice, ",") 34 | } 35 | -------------------------------------------------------------------------------- /flag_string.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | type stringValue struct { 4 | val *string 5 | } 6 | 7 | func (v *stringValue) Set(value string) error { 8 | *v.val = value 9 | return nil 10 | } 11 | 12 | func (v *stringValue) String() string { 13 | return *v.val 14 | } 15 | -------------------------------------------------------------------------------- /flag_string_slice.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "strings" 4 | 5 | type stringSliceValue struct { 6 | val *[]string 7 | } 8 | 9 | func (v *stringSliceValue) Set(value string) error { 10 | *v.val = append(*v.val, value) 11 | return nil 12 | } 13 | 14 | func (v *stringSliceValue) String() string { 15 | return strings.Join(*v.val, ",") 16 | } 17 | -------------------------------------------------------------------------------- /flag_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | "os" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestFlagInitTypes(t *testing.T) { 12 | vals := []interface{}{ 13 | new(bool), 14 | new(string), 15 | new([]string), 16 | new(int), 17 | new([]int), 18 | new(int8), 19 | new(int16), 20 | new(int32), 21 | new(int64), 22 | new(uint), 23 | new([]uint), 24 | new(uint8), 25 | new(uint16), 26 | new(uint32), 27 | new(uint64), 28 | new(float32), 29 | new(float64), 30 | new([]float64), 31 | new(time.Time), 32 | new(time.Duration), 33 | new(time.Location), 34 | new(net.IP), 35 | new([]net.IP), 36 | new(net.IPMask), 37 | new(net.IPNet), 38 | new([]net.IPNet), 39 | new(url.URL), 40 | new([]url.URL), 41 | new(stringValue), 42 | } 43 | 44 | for _, v := range vals { 45 | f := &Flag{ 46 | Value: v, 47 | } 48 | f.initialize() 49 | } 50 | } 51 | 52 | func TestFlagInitEnvVar(t *testing.T) { 53 | v := new(string) 54 | 55 | os.Setenv("TEST_E2", "ee") 56 | f := &Flag{ 57 | Value: v, 58 | EnvVar: "TEST_E1, TEST_E2", 59 | } 60 | 61 | f.initialize() 62 | 63 | if f.GetValue() != "ee" { 64 | t.Fatal("EnvVar is wrong") 65 | } 66 | } 67 | 68 | func TestFlagInitDefValue(t *testing.T) { 69 | v := new(string) 70 | 71 | f := &Flag{ 72 | Value: v, 73 | DefValue: "ee", 74 | } 75 | 76 | f.initialize() 77 | 78 | if f.GetValue() != "ee" { 79 | t.Fatal("DefVar is wrong") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /flag_time.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | type timeValue struct { 9 | val *time.Time 10 | } 11 | 12 | func (v *timeValue) Set(value string) error { 13 | val, err := stringToTime(value) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | *v.val = *val 19 | return nil 20 | } 21 | 22 | func (v *timeValue) String() string { 23 | return (*v.val).String() 24 | } 25 | 26 | var timePatterns = []string{ 27 | time.RFC3339, 28 | time.RFC3339Nano, 29 | "2006-01-02T15:04:05", // iso8601 without timezone 30 | time.RFC1123Z, 31 | time.RFC1123, 32 | "2006-01-02 15:04:05.999999999 -0700 MST", 33 | time.RFC822Z, 34 | time.RFC822, 35 | time.ANSIC, 36 | time.UnixDate, 37 | time.RubyDate, 38 | "2006-01-02 15:04:05Z07:00", 39 | "2006-01-02 15:04:05", 40 | "02 Jan 06 15:04 MST", 41 | "2006-01-02", 42 | "02 Jan 2006", 43 | "2006-01-02 15:04:05 -07:00", 44 | "2006-01-02 15:04:05 -0700", 45 | } 46 | 47 | func stringToTime(s string) (*time.Time, error) { 48 | for _, pattern := range timePatterns { 49 | if d, err := time.Parse(pattern, s); err == nil { 50 | return &d, nil 51 | } 52 | } 53 | return nil, fmt.Errorf("unable to parse date: %s", s) 54 | } 55 | -------------------------------------------------------------------------------- /flag_time_duration.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "time" 4 | 5 | type timeDurationValue struct { 6 | val *time.Duration 7 | } 8 | 9 | func (v *timeDurationValue) Set(value string) error { 10 | val, err := time.ParseDuration(value) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | *v.val = val 16 | return nil 17 | } 18 | 19 | func (v *timeDurationValue) String() string { 20 | return (*v.val).String() 21 | } 22 | -------------------------------------------------------------------------------- /flag_time_location.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import "time" 4 | 5 | type timeLocationValue struct { 6 | val *time.Location 7 | } 8 | 9 | func (v *timeLocationValue) Set(value string) error { 10 | val, err := time.LoadLocation(value) 11 | if err != nil { 12 | return err 13 | } 14 | 15 | *v.val = *val 16 | return nil 17 | } 18 | 19 | func (v *timeLocationValue) String() string { 20 | return (*v.val).String() 21 | } 22 | -------------------------------------------------------------------------------- /flag_uint.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type uintValue struct { 8 | val *uint 9 | } 10 | 11 | func (v *uintValue) Set(value string) error { 12 | val, err := strconv.ParseUint(value, 0, 0) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = uint(val) 18 | return nil 19 | } 20 | 21 | func (v *uintValue) String() string { 22 | return strconv.FormatUint(uint64(*v.val), 10) 23 | } 24 | -------------------------------------------------------------------------------- /flag_uint16.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type uint16Value struct { 8 | val *uint16 9 | } 10 | 11 | func (v *uint16Value) Set(value string) error { 12 | val, err := strconv.ParseUint(value, 0, 16) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = uint16(val) 18 | return nil 19 | } 20 | 21 | func (v *uint16Value) String() string { 22 | return strconv.FormatUint(uint64(*v.val), 10) 23 | } 24 | -------------------------------------------------------------------------------- /flag_uint32.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type uint32Value struct { 8 | val *uint32 9 | } 10 | 11 | func (v *uint32Value) Set(value string) error { 12 | val, err := strconv.ParseUint(value, 0, 32) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = uint32(val) 18 | return nil 19 | } 20 | 21 | func (v *uint32Value) String() string { 22 | return strconv.FormatUint(uint64(*v.val), 10) 23 | } 24 | -------------------------------------------------------------------------------- /flag_uint64.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type uint64Value struct { 8 | val *uint64 9 | } 10 | 11 | func (v *uint64Value) Set(value string) error { 12 | val, err := strconv.ParseUint(value, 0, 64) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = uint64(val) 18 | return nil 19 | } 20 | 21 | func (v *uint64Value) String() string { 22 | return strconv.FormatUint(uint64(*v.val), 10) 23 | } 24 | -------------------------------------------------------------------------------- /flag_uint8.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type uint8Value struct { 8 | val *uint8 9 | } 10 | 11 | func (v *uint8Value) Set(value string) error { 12 | val, err := strconv.ParseUint(value, 0, 8) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = uint8(val) 18 | return nil 19 | } 20 | 21 | func (v *uint8Value) String() string { 22 | return strconv.FormatUint(uint64(*v.val), 10) 23 | } 24 | -------------------------------------------------------------------------------- /flag_uint_slice.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | type uintSliceValue struct { 9 | val *[]uint 10 | } 11 | 12 | func (v *uintSliceValue) Set(value string) error { 13 | val, err := strconv.ParseUint(value, 0, 0) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | *v.val = append(*v.val, uint(val)) 19 | return nil 20 | } 21 | 22 | func (v *uintSliceValue) String() string { 23 | l := len(*v.val) 24 | if l == 0 { 25 | return "" 26 | } 27 | 28 | slice := make([]string, 0, l) 29 | for _, val := range *v.val { 30 | s := strconv.FormatUint(uint64(val), 10) 31 | slice = append(slice, s) 32 | } 33 | return strings.Join(slice, ",") 34 | } 35 | -------------------------------------------------------------------------------- /flag_url.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | type urlValue struct { 8 | val *url.URL 9 | } 10 | 11 | func (v *urlValue) Set(value string) error { 12 | val, err := url.Parse(value) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | *v.val = *val 18 | return nil 19 | } 20 | 21 | func (v *urlValue) String() string { 22 | return (*v.val).String() 23 | } 24 | -------------------------------------------------------------------------------- /flag_url_slice.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | type urlSliceValue struct { 9 | val *[]url.URL 10 | } 11 | 12 | func (v *urlSliceValue) Set(value string) error { 13 | val, err := url.Parse(value) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | *v.val = append(*v.val, *val) 19 | return nil 20 | } 21 | 22 | func (v *urlSliceValue) String() string { 23 | l := len(*v.val) 24 | if l == 0 { 25 | return "" 26 | } 27 | 28 | slice := make([]string, 0, l) 29 | for _, val := range *v.val { 30 | slice = append(slice, val.String()) 31 | } 32 | return strings.Join(slice, ",") 33 | } 34 | -------------------------------------------------------------------------------- /flag_value_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "net" 5 | "net/url" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestFlagSet(t *testing.T) { 11 | tests := []struct { 12 | val string 13 | wrap Value 14 | want string 15 | }{ 16 | {"-1", &intValue{new(int)}, ""}, 17 | {"-1", &int8Value{new(int8)}, ""}, 18 | {"-1", &int16Value{new(int16)}, ""}, 19 | {"-1", &int32Value{new(int32)}, ""}, 20 | {"-1", &int64Value{new(int64)}, ""}, 21 | 22 | {"1", &uintValue{new(uint)}, ""}, 23 | {"1", &uint8Value{new(uint8)}, ""}, 24 | {"1", &uint16Value{new(uint16)}, ""}, 25 | {"1", &uint32Value{new(uint32)}, ""}, 26 | {"1", &uint64Value{new(uint64)}, ""}, 27 | 28 | {"1.1", &float32Value{new(float32)}, ""}, 29 | {"1.1", &float64Value{new(float64)}, ""}, 30 | 31 | {"true", &boolValue{new(bool)}, ""}, 32 | {"no", &boolValue{new(bool)}, "false"}, 33 | 34 | {"abc", &stringValue{new(string)}, ""}, 35 | 36 | {"2018-05-24 14:56:56 +0000 UTC", &timeValue{new(time.Time)}, ""}, 37 | {"1h2m30s", &timeDurationValue{new(time.Duration)}, ""}, 38 | {"Asia/Shanghai", &timeLocationValue{new(time.Location)}, ""}, 39 | 40 | {"127.0.0.1", &ipValue{new(net.IP)}, ""}, 41 | {"255.255.0.0", &ipMaskValue{new(net.IPMask)}, "ffff0000"}, 42 | {"192.0.2.0/24", &ipNetValue{new(net.IPNet)}, ""}, 43 | 44 | {"http://google.com/", &urlValue{new(url.URL)}, ""}, 45 | } 46 | 47 | for _, tt := range tests { 48 | err := tt.wrap.Set(tt.val) 49 | if err != nil { 50 | t.Errorf("%T flag set err: %v", tt.wrap, err) 51 | } else { 52 | want := tt.want 53 | if want == "" { 54 | want = tt.val 55 | } 56 | if got := tt.wrap.String(); got != want { 57 | t.Errorf("%T flag set value is wrong: Got %q, Want: %q", tt.wrap, got, want) 58 | } 59 | } 60 | } 61 | } 62 | 63 | func TestFlagSliceSet(t *testing.T) { 64 | tests := []struct { 65 | val1 string 66 | val2 string 67 | wrap Value 68 | want string 69 | }{ 70 | {"1", "2", &intSliceValue{new([]int)}, ""}, 71 | {"1", "2", &uintSliceValue{new([]uint)}, ""}, 72 | {"1.1", "2.2", &float64SliceValue{new([]float64)}, ""}, 73 | {"a", "b", &stringSliceValue{new([]string)}, ""}, 74 | {"127.0.0.1", "127.0.0.2", &ipSliceValue{new([]net.IP)}, ""}, 75 | {"192.0.2.0/24", "192.168.0.0/16", &ipNetSliceValue{new([]net.IPNet)}, ""}, 76 | {"http://google.com/", "http://baidu.com/", &urlSliceValue{new([]url.URL)}, ""}, 77 | } 78 | 79 | for _, tt := range tests { 80 | err1 := tt.wrap.Set(tt.val1) 81 | err2 := tt.wrap.Set(tt.val2) 82 | if err1 != nil { 83 | t.Errorf("%T slice flag set err: %v", tt.wrap, err1) 84 | } else if err2 != nil { 85 | t.Errorf("%T slice flag set err: %v", tt.wrap, err2) 86 | } else { 87 | want := tt.want 88 | if want == "" { 89 | want = tt.val1 + "," + tt.val2 90 | } 91 | if got := tt.wrap.String(); got != want { 92 | t.Errorf("%T slice flag set value is wrong: Got %q, Want: %q", tt.wrap, got, want) 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/subchen/go-cli/v3 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "runtime" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | // HelpTemplate is the text template for the Default help topic. 13 | // go-cli uses text/template to render templates. You can 14 | // render custom help text by setting this variable. 15 | var HelpTemplate = `NAME: 16 | {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} 17 | 18 | USAGE: 19 | {{- range .UsageTextLines}} 20 | {{$.Name}} {{.}} 21 | {{- end}}{{if .Version}} 22 | 23 | VERSION: 24 | {{.Version}}{{end}}{{if .Description}} 25 | 26 | DESCRIPTION: 27 | {{.Description}}{{end}}{{if .AuthorLines}} 28 | 29 | AUTHORS: 30 | {{- range .AuthorLines}} 31 | {{.}} 32 | {{- end}}{{end}}{{if .VisibleCommands}} 33 | 34 | COMMANDS: 35 | {{- range .VisibleCommandsUsageLines}} 36 | {{.}} 37 | {{- end}}{{end}}{{if .VisibleFlags}} 38 | 39 | {{if .VisibleCommands }}GLOBALS {{end}}OPTIONS: 40 | {{- range .VisibleFlagsUsageLines}} 41 | {{.}} 42 | {{- end}}{{end}}{{if .ExampleLines}} 43 | 44 | EXAMPLES: 45 | {{- range .ExampleLines}} 46 | {{.}} 47 | {{- end}}{{end}}{{if .SeeAlsoLines}} 48 | 49 | SEE ALSO: 50 | {{- range .SeeAlsoLines}} 51 | {{.}} 52 | {{- end}}{{end}}{{if .VisibleCommands}} 53 | 54 | Run '{{.Name}} COMMAND --help' for more information on a command.{{end}} 55 | 56 | ` 57 | 58 | // helpWriter variable for testing hook 59 | var helpWriter io.Writer = os.Stdout 60 | 61 | // HelpContext is a struct for output help 62 | type HelpContext struct { 63 | Name string 64 | Version string 65 | Usage string 66 | UsageText string 67 | Description string 68 | Authors string 69 | Examples string 70 | SeeAlso string 71 | Flags []*Flag 72 | Commands []*Command 73 | } 74 | 75 | func newAppHelpContext(name string, app *App) *HelpContext { 76 | return &HelpContext{ 77 | Name: name, 78 | Version: app.Version, 79 | Usage: app.Usage, 80 | UsageText: app.UsageText, 81 | Description: app.Description, 82 | Authors: app.Authors, 83 | Examples: app.Examples, 84 | SeeAlso: app.SeeAlso, 85 | Flags: app.Flags, 86 | Commands: app.Commands, 87 | } 88 | } 89 | 90 | func newCommandHelpContext(name string, cmd *Command, app *App) *HelpContext { 91 | return &HelpContext{ 92 | Name: name, 93 | Usage: cmd.Usage, 94 | UsageText: cmd.UsageText, 95 | Description: cmd.Description, 96 | Examples: cmd.Examples, 97 | SeeAlso: cmd.SeeAlso, 98 | Flags: cmd.Flags, 99 | Commands: cmd.Commands, 100 | } 101 | } 102 | 103 | // Level return command/subcommand's level 104 | func (c *HelpContext) Level() int { 105 | return strings.Count(c.Name, " ") 106 | } 107 | 108 | // VisibleFlags returns flags which are visible 109 | func (c *HelpContext) VisibleFlags() []*Flag { 110 | flags := make([]*Flag, 0, len(c.Flags)) 111 | for _, f := range c.Flags { 112 | if !f.Hidden { 113 | flags = append(flags, f) 114 | } 115 | } 116 | return flags 117 | } 118 | 119 | // VisibleCommands returns commands which are visible 120 | func (c *HelpContext) VisibleCommands() []*Command { 121 | commands := make([]*Command, 0, len(c.Commands)) 122 | for _, c := range c.Commands { 123 | if !c.Hidden { 124 | commands = append(commands, c) 125 | } 126 | } 127 | return commands 128 | } 129 | 130 | // UsageTextLines splits line for usage 131 | func (c *HelpContext) UsageTextLines() []string { 132 | if len(c.UsageText) == 0 { 133 | usage := "" 134 | if len(c.VisibleCommands()) > 0 { 135 | if len(c.VisibleFlags()) > 0 { 136 | usage = usage + "[global options] " 137 | } 138 | usage = usage + "COMMAND [command options] [arguments ...]" 139 | } else { 140 | if len(c.VisibleFlags()) > 0 { 141 | if c.Level() == 0 { 142 | usage = usage + "[options] " 143 | } else { 144 | usage = usage + "[command options] " 145 | } 146 | } 147 | usage = usage + "[arguments ...]" 148 | } 149 | c.UsageText = usage 150 | } 151 | 152 | usages := strings.Split(c.UsageText, "\n") 153 | for i, usage := range usages { 154 | usages[i] = strings.TrimSpace(usage) 155 | } 156 | return usages 157 | } 158 | 159 | // AuthorLines splits line for authors 160 | func (c *HelpContext) AuthorLines() []string { 161 | if len(c.Authors) == 0 { 162 | return nil 163 | } 164 | authors := strings.Split(c.Authors, "\n") 165 | for i, author := range authors { 166 | authors[i] = strings.TrimSpace(author) 167 | } 168 | return authors 169 | } 170 | 171 | // ExampleLines splits line for examples 172 | func (c *HelpContext) ExampleLines() []string { 173 | c.Examples = strings.TrimSpace(c.Examples) 174 | if len(c.Examples) == 0 { 175 | return nil 176 | } 177 | examples := strings.Split(c.Examples, "\n") 178 | for i, example := range examples { 179 | examples[i] = strings.TrimSpace(example) 180 | } 181 | return examples 182 | } 183 | 184 | // SeeAlsoLines splits line for see also 185 | func (c *HelpContext) SeeAlsoLines() []string { 186 | c.SeeAlso = strings.TrimSpace(c.SeeAlso) 187 | if len(c.SeeAlso) == 0 { 188 | return nil 189 | } 190 | seeAlso := strings.Split(c.SeeAlso, "\n") 191 | for i, example := range seeAlso { 192 | seeAlso[i] = strings.TrimSpace(example) 193 | } 194 | return seeAlso 195 | } 196 | 197 | // VisibleFlagsUsageLines splits line for flags 198 | func (c *HelpContext) VisibleFlagsUsageLines() []string { 199 | flags := c.VisibleFlags() 200 | 201 | // long flag is indent if short flag is exists. 202 | longIndent := false 203 | outer: 204 | for _, f := range flags { 205 | for _, name := range f.Names() { 206 | if len(name) == 1 { 207 | longIndent = true 208 | break outer 209 | } 210 | } 211 | } 212 | 213 | // calc max width for option name 214 | max := 0 215 | for _, f := range flags { 216 | label := makeFlagLabel(f, longIndent) 217 | if len(label) > max { 218 | max = len(label) 219 | } 220 | } 221 | 222 | usageLines := make([]string, 0, len(flags)) 223 | for _, f := range flags { 224 | label := makeFlagLabel(f, longIndent) 225 | usage := f.Usage 226 | whitespaces := strings.Repeat(" ", max-len(label)) 227 | if f.DefValue != "" { 228 | usage = usage + " (default: " + f.DefValue + ")" 229 | } 230 | if f.EnvVar != "" { 231 | usage = usage + " (Env: " + f.EnvVar + ")" 232 | } 233 | line := fmt.Sprintf("%s%s %s", label, whitespaces, usage) 234 | usageLines = append(usageLines, line) 235 | } 236 | return usageLines 237 | } 238 | 239 | // VisibleCommandsUsageLines splits line for commands 240 | func (c *HelpContext) VisibleCommandsUsageLines() []string { 241 | // calc max width for command name 242 | max := 0 243 | commands := c.VisibleCommands() 244 | for _, c := range commands { 245 | label := makeCommandLabel(c) 246 | if len(label) > max { 247 | max = len(label) 248 | } 249 | } 250 | 251 | usageLines := make([]string, 0, len(commands)) 252 | for _, c := range commands { 253 | label := makeCommandLabel(c) 254 | whitespaces := strings.Repeat(" ", max-len(label)) 255 | line := fmt.Sprintf("%s%s %s", label, whitespaces, c.Usage) 256 | usageLines = append(usageLines, line) 257 | } 258 | return usageLines 259 | } 260 | 261 | func makeFlagLabel(f *Flag, longIndent bool) string { 262 | names := f.Names() 263 | 264 | value := "" 265 | if !f.IsBool { 266 | if f.NoOptDefValue != "" { 267 | value = " [" + f.Placeholder + "]" 268 | } else { 269 | value = " " + f.Placeholder 270 | } 271 | } 272 | 273 | labels := make([]string, 0, len(names)) 274 | for _, name := range names { 275 | label := "-" + name 276 | if len(name) > 1 { 277 | label = "-" + label 278 | } 279 | labels = append(labels, label) 280 | } 281 | 282 | str := strings.Join(labels, ", ") + value 283 | if longIndent && strings.HasPrefix(str, "--") { 284 | str = " " + str 285 | } 286 | 287 | return str 288 | } 289 | 290 | func makeCommandLabel(c *Command) string { 291 | return strings.Join(c.Names(), ", ") 292 | } 293 | 294 | func showHelp(c *HelpContext) { 295 | tmpl, err := template.New("help").Parse(HelpTemplate) 296 | if err != nil { 297 | panic(err) 298 | } 299 | err = tmpl.Execute(helpWriter, c) 300 | if err != nil { 301 | panic(err) 302 | } 303 | } 304 | 305 | func showVersion(app *App) { 306 | fmt.Fprintf(helpWriter, "Name: %s\n", app.Name) 307 | fmt.Fprintf(helpWriter, "Version: %s\n", app.Version) 308 | 309 | if build := app.BuildInfo; build != nil { 310 | if build.GitRevCount != "" { 311 | fmt.Fprintf(helpWriter, "Patches: %s\n", build.GitRevCount) 312 | } 313 | if build.GitBranch != "" { 314 | fmt.Fprintf(helpWriter, "Git branch: %s\n", build.GitBranch) 315 | } 316 | if build.GitCommit != "" { 317 | fmt.Fprintf(helpWriter, "Git commit: %s\n", build.GitCommit) 318 | } 319 | if build.Timestamp != "" { 320 | fmt.Fprintf(helpWriter, "Built: %s\n", build.Timestamp) 321 | } 322 | } 323 | 324 | fmt.Fprintf(helpWriter, "Go version: %s\n", runtime.Version()) 325 | fmt.Fprintf(helpWriter, "OS/Arch: %s/%v\n", runtime.GOOS, runtime.GOARCH) 326 | } 327 | -------------------------------------------------------------------------------- /help_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestHelpShowVersion(t *testing.T) { 9 | app := &App{ 10 | Name: "app", 11 | Version: "1.2.3", 12 | BuildInfo: &BuildInfo{ 13 | Timestamp: "Sat May 13 19:53:08 UTC 2017", 14 | GitBranch: "master", 15 | GitCommit: "320279c", 16 | GitRevCount: "1234", 17 | }, 18 | } 19 | 20 | // reset 21 | helpWriter = new(bytes.Buffer) 22 | 23 | showVersion(app) 24 | } 25 | 26 | func TestHelpShowHelp(t *testing.T) { 27 | app := NewApp() 28 | app.Name = "app" 29 | app.Version = "1.1.1" 30 | app.Usage = "demo app" 31 | app.Authors = "Guoqiang Chen " 32 | 33 | app.Flags = []*Flag{ 34 | { 35 | Name: "i, input", 36 | Usage: "input file", 37 | Placeholder: "file", 38 | }, 39 | { 40 | Name: "o, output", 41 | Usage: "output file", 42 | }, 43 | } 44 | 45 | app.Commands = []*Command{ 46 | { 47 | Name: "build", 48 | Usage: "build project", 49 | Flags: []*Flag{ 50 | { 51 | Name: "debug", 52 | Usage: "enable debug", 53 | IsBool: true, 54 | }, 55 | }, 56 | SeeAlso: "https://github.com/subchen/go-cli#build\nhttps://github.com/subchen/go-cli#build2", 57 | }, 58 | { 59 | Name: "release", 60 | Usage: "release project", 61 | }, 62 | } 63 | 64 | app.SeeAlso = `https://github.com/subchen 65 | https://github.com/yingzhuo` 66 | 67 | // reset 68 | helpWriter = new(bytes.Buffer) 69 | 70 | ctx1 := newAppHelpContext("app", app) 71 | showHelp(ctx1) 72 | 73 | ctx2 := newCommandHelpContext("app build", app.Commands[0], app) 74 | showHelp(ctx2) 75 | } 76 | --------------------------------------------------------------------------------