diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..4f138e8 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# See https://help.github.com/articles/about-codeowners/ +# for more info about CODEOWNERS file + +* @urfave/cli diff --git a/.gitignore b/.gitignore index faf70c4..7a7e2d9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.coverprofile node_modules/ +vendor \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 58ed292..e500b38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,28 +1,35 @@ language: go sudo: false -dist: trusty -osx_image: xcode8.3 -go: 1.11.x +dist: bionic +osx_image: xcode10 +go: + - 1.11.x + - 1.12.x + - 1.13.x os: -- linux -- osx + - linux + - osx + +env: + GO111MODULE=on + GOPROXY=https://proxy.golang.org cache: directories: - - node_modules + - node_modules before_script: -- if [[ $(uname) == Darwin ]]; then - sudo pip2 install flake8; - else - pip install --user flake8; - fi -- mkdir -p ${GOPATH%%:*}/src/gopkg.in/urfave -- rm -rvf ${GOPATH%%:*}/src/gopkg.in/urfave/cli.v2 -- rm -rvf ${GOPATH%%:*}/pkg/*/gopkg.in/urfave/cli.v2.a -- ln -sv ${TRAVIS_BUILD_DIR} ${GOPATH%%:*}/src/gopkg.in/urfave/cli.v2 + - go get github.com/urfave/gfmrun/cmd/gfmrun + - go get golang.org/x/tools/cmd/goimports + - npm install markdown-toc + - go mod tidy script: -- flake8 runtests cli-v1-to-v2 generate-flag-types -- make all + - go run build.go vet + - go run build.go test + - go run build.go gfmrun + - go run build.go toc + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fa0ca0..d1b5c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,11 +39,110 @@ ### Removed +## [1.22.1] - 2019-09-11 + ### Fixed -### Deprecated +* Hide output of hidden commands on man pages in [urfave/cli/pull/889](https://github.com/urfave/cli/pull/889) via [@crosbymichael](https://github.com/crosbymichael) +* Don't generate fish completion for hidden commands [urfave/cli/pull/891](https://github.com/urfave/891) via [@saschagrunert](https://github.com/saschagrunert) +* Using short flag names for required flags throws an error in [urfave/cli/pull/890](https://github.com/urfave/cli/pull/890) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) + +### Changed + +* Remove flag code generation logic, legacy python test runner in [urfave/cli/pull/883](https://github.com/urfave/cli/pull/883) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) +* Enable Go Modules support, drop support for `Go 1.10` add support for `Go 1.13` in [urfave/cli/pull/885](https://github.com/urfave/cli/pull/885) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) + +## [1.22.0] - 2019-09-07 + +### Fixed + +* Fix Subcommands not falling back to `app.ExitEventHandler` in [urfave/cli/pull/856](https://github.com/urfave/cli/pull/856) via [@FaranIdo](https://github.com/FaranIdo) + +### Changed + +* Clarify that altsrc supports both TOML and JSON in [urfave/cli/pull/774](https://github.com/urfave/cli/pull/774) via [@whereswaldon](https://github.com/whereswaldon) +* Made the exit code example more clear in [urfave/cli/pull/823](https://github.com/urfave/cli/pull/823) via [@xordspar0](https://github.com/xordspar0) +* Removed the use of python for internal flag generation in [urfave/cli/pull/836](https://github.com/urfave/cli/pull/836) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) +* Changed the supported go versions to `1.10`, `1.11`, `1.12` in [urfave/cli/pull/843](https://github.com/urfave/cli/pull/843) via [@lafriks](https://github.com/lafriks) +* Changed the v1 releases section in the readme in [urfave/cli/pull/862](https://github.com/urfave/cli/pull/862) via [@russoj88](https://github.com/russoj88) +* Cleaned up go modules in [urfave/cli/pull/874](https://github.com/urfave/cli/pull/874) via [@saschagrunert](https://github.com/saschagrunert) + +### Added + +* Added `UseShortOptionHandling` for combining short flags in [urfave/cli/pull/735](https://github.com/urfave/cli/pull/735) via [@rliebz](https://github.com/rliebz) +* Added support for flags bash completion in [urfave/cli/pull/808](https://github.com/urfave/cli/pull/808) via [@yogeshlonkar](https://github.com/yogeshlonkar) +* Added the `TakesFile` indicator to flag in [urfave/cli/pull/851](https://github.com/urfave/cli/pull/851) via [@saschagrunert](https://github.com/saschagrunert) +* Added fish shell completion support in [urfave/cli/pull/848](https://github.com/urfave/cli/pull/848) via [@saschagrunert](https://github.com/saschagrunert) + +## [1.21.0] - 2019-08-02 + +### Fixed + +* Fix using "slice" flag types with `EnvVar` in [urfave/cli/pull/687](https://github.com/urfave/cli/pull/687) via [@joshuarubin](https://github.com/joshuarubin) +* Fix regression of `SkipFlagParsing` behavior in [urfave/cli/pull/697](https://github.com/urfave/cli/pull/697) via [@jszwedko](https://github.com/jszwedko) +* Fix handling `ShortOptions` and `SkipArgReorder` in [urfave/cli/pull/686](https://github.com/urfave/cli/pull/686) via [@baude](https://github.com/baude) +* Fix args reordering when bool flags are present in [urfave/cli/pull/712](https://github.com/urfave/cli/pull/712) via [@windler](https://github.com/windler) +* Fix parsing of short options in [urfave/cli/pull/758](https://github.com/urfave/cli/pull/758) via [@vrothberg](https://github.com/vrothberg) +* Fix unaligned indents for the command help messages in [urfave/cli/pull/806](https://github.com/urfave/cli/pull/806) via [@mingrammer](https://github.com/mingrammer) + +### Changed + +* Cleaned up help output in [urfave/cli/pull/664](https://github.com/urfave/cli/pull/664) via [@maguro](https://github.com/maguro) +* Remove redundant nil checks in [urfave/cli/pull/773](https://github.com/urfave/cli/pull/773) via [@teresy](https://github.com/teresy) +* Case is now considered when sorting strings in [urfave/cli/pull/676](https://github.com/urfave/cli/pull/676) via [@rliebz](https://github.com/rliebz) + +### Added + +* Added _"required flags"_ support in [urfave/cli/pull/819](https://github.com/urfave/cli/pull/819) via [@lynncyrin](https://github.com/lynncyrin/) +* Backport JSON `InputSource` to v1 in [urfave/cli/pull/598](https://github.com/urfave/cli/pull/598) via [@jszwedko](https://github.com/jszwedko) +* Allow more customization of flag help strings in [urfave/cli/pull/661](https://github.com/urfave/cli/pull/661) via [@rliebz](https://github.com/rliebz) +* Allow custom `ExitError` handler function in [urfave/cli/pull/628](https://github.com/urfave/cli/pull/628) via [@phinnaeus](https://github.com/phinnaeus) +* Allow loading a variable from a file in [urfave/cli/pull/675](https://github.com/urfave/cli/pull/675) via [@jmccann](https://github.com/jmccann) +* Allow combining short bool names in [urfave/cli/pull/684](https://github.com/urfave/cli/pull/684) via [@baude](https://github.com/baude) +* Added test coverage to context in [urfave/cli/pull/788](https://github.com/urfave/cli/pull/788) via [@benzvan](https://github.com/benzvan) +* Added go module support in [urfave/cli/pull/831](https://github.com/urfave/cli/pull/831) via [@saschagrunert](https://github.com/saschagrunert) + +## [1.20.0] - 2017-08-10 + +### Fixed + +* `HandleExitCoder` is now correctly iterates over all errors in + a `MultiError`. The exit code is the exit code of the last error or `1` if + there are no `ExitCoder`s in the `MultiError`. +* Fixed YAML file loading on Windows (previously would fail validate the file path) +* Subcommand `Usage`, `Description`, `ArgsUsage`, `OnUsageError` correctly + propogated +* `ErrWriter` is now passed downwards through command structure to avoid the + need to redefine it +* Pass `Command` context into `OnUsageError` rather than parent context so that + all fields are avaiable +* Errors occuring in `Before` funcs are no longer double printed +* Use `UsageText` in the help templates for commands and subcommands if + defined; otherwise build the usage as before (was previously ignoring this + field) +* `IsSet` and `GlobalIsSet` now correctly return whether a flag is set if + a program calls `Set` or `GlobalSet` directly after flag parsing (would + previously only return `true` if the flag was set during parsing) + +### Changed + +* No longer exit the program on command/subcommand error if the error raised is + not an `OsExiter`. This exiting behavior was introduced in 1.19.0, but was + determined to be a regression in functionality. See [the + PR](https://github.com/urfave/cli/pull/595) for discussion. + +### Added -### Security +* `CommandsByName` type was added to make it easy to sort `Command`s by name, + alphabetically +* `altsrc` now handles loading of string and int arrays from TOML +* Support for definition of custom help templates for `App` via + `CustomAppHelpTemplate` +* Support for arbitrary key/value fields on `App` to be used with + `CustomAppHelpTemplate` via `ExtraInfo` +* `HelpFlag`, `VersionFlag`, and `BashCompletionFlag` changed to explictly be + `cli.Flag`s allowing for the use of custom flags satisfying the `cli.Flag` + interface to be used. ## [1.19.1] - 2016-11-21 ### Fixed @@ -403,7 +502,13 @@ signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. ### Added - Initial implementation. -[Unreleased]: https://github.com/urfave/cli/compare/v1.18.0...HEAD +[Unreleased]: https://github.com/urfave/cli/compare/v1.22.1...HEAD +[1.22.1]: https://github.com/urfave/cli/compare/v1.22.0...v1.22.1 +[1.22.0]: https://github.com/urfave/cli/compare/v1.21.0...v1.22.0 +[1.21.0]: https://github.com/urfave/cli/compare/v1.20.0...v1.21.0 +[1.20.0]: https://github.com/urfave/cli/compare/v1.19.1...v1.20.0 +[1.19.1]: https://github.com/urfave/cli/compare/v1.19.0...v1.19.1 +[1.19.0]: https://github.com/urfave/cli/compare/v1.18.0...v1.19.0 [1.18.0]: https://github.com/urfave/cli/compare/v1.17.0...v1.18.0 [1.17.0]: https://github.com/urfave/cli/compare/v1.16.0...v1.17.0 [1.16.0]: https://github.com/urfave/cli/compare/v1.15.0...v1.16.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..41ba294 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting Dan Buch at dan@meatballhat.com. All complaints will be +reviewed and investigated and will result in a response that is deemed necessary +and appropriate to the circumstances. The project team is obligated to maintain +confidentiality with regard to the reporter of an incident. Further details of +specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9a4640a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +## Contributing + +Use @urfave/cli to ping the maintainers. + +Feel free to put up a pull request to fix a bug or maybe add a feature. We will +give it a code review and make sure that it does not break backwards +compatibility. If collaborators agree that it is in line with +the vision of the project, we will work with you to get the code into +a mergeable state and merge it into the master branch. + +If you have contributed something significant to the project, we will most +likely add you as a collaborator. As a collaborator you are given the ability +to merge others pull requests. It is very important that new code does not +break existing code, so be careful about what code you do choose to merge. + +If you feel like you have contributed to the project but have not yet been added +as a collaborator, we probably forgot to add you :sweat_smile:. Please open an +issue! diff --git a/README.md b/README.md index dbe1bf3..7624c48 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,11 @@ cli [![Build Status](https://travis-ci.org/urfave/cli.svg?branch=master)](https://travis-ci.org/urfave/cli) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/rtgk5xufi932pb2v?svg=true)](https://ci.appveyor.com/project/urfave/cli) + [![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://godoc.org/github.com/urfave/cli) [![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) [![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) -[![top level coverage](https://gocover.io/_badge/github.com/urfave/cli?0 "top level coverage")](http://gocover.io/github.com/urfave/cli) / -[![altsrc coverage](https://gocover.io/_badge/github.com/urfave/cli/altsrc?0 "altsrc coverage")](http://gocover.io/github.com/urfave/cli/altsrc) - -**Notice:** This is the library formerly known as -`github.com/codegangsta/cli` -- Github will automatically redirect requests -to this repository, but we recommend updating your references for clarity. +[![codecov](https://codecov.io/gh/urfave/cli/branch/master/graph/badge.svg)](https://codecov.io/gh/urfave/cli) cli is a simple, fast, and fun package for building command line apps in Go. The goal is to enable developers to write fast and distributable command line @@ -23,7 +19,7 @@ applications in an expressive way. - [Installation](#installation) * [Supported platforms](#supported-platforms) * [Using the `v2` branch](#using-the-v2-branch) - * [Pinning to the `v1` releases](#pinning-to-the-v1-releases) + * [Using `v1` releases](#using-v1-releases) - [Getting Started](#getting-started) - [Examples](#examples) * [Arguments](#arguments) @@ -32,12 +28,22 @@ applications in an expressive way. + [Alternate Names](#alternate-names) + [Ordering](#ordering) + [Values from the Environment](#values-from-the-environment) + + [Values from files](#values-from-files) + [Values from alternate input sources (YAML, TOML, and others)](#values-from-alternate-input-sources-yaml-toml-and-others) +<<<<<<< HEAD + [Default Values for help output](#default-values-for-help-output) * [Subcommands](#subcommands) * [Subcommands categories](#subcommands-categories) * [Exit code](#exit-code) * [Shell Completion](#shell-completion) +======= + + [Precedence](#precedence) + * [Subcommands](#subcommands) + * [Subcommands categories](#subcommands-categories) + * [Exit code](#exit-code) + * [Combining short options](#combining-short-options) + * [Bash Completion](#bash-completion) +>>>>>>> master + [Enabling](#enabling) + [Distribution](#distribution) + [Customization](#customization) @@ -62,12 +68,12 @@ organized, and expressive! ## Installation -Make sure you have a working Go environment. Go version 1.2+ is supported. [See +Make sure you have a working Go environment. Go version 1.10+ is supported. [See the install instructions for Go](http://golang.org/doc/install.html). To install cli, simply run: ``` -$ go get github.com/urfave/cli +$ go get github.com/urfave/cli/v2 ``` Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can @@ -91,21 +97,21 @@ the new `master` branch once development there has settled down. The current `master` branch (mirrored as `v1`) is being manually merged into `v2` on an irregular human-based schedule, but generally if one wants to "upgrade" to `v2` *now* and accept the volatility (read: "awesomeness") that comes along with -that, please use whatever version pinning of your preference, such as via -`gopkg.in`: +that, please use: ``` -$ go get gopkg.in/urfave/cli.v2 +$ go get github.com/urfave/cli/v2 ``` ``` go ... import ( - "gopkg.in/urfave/cli.v2" // imports as package "cli" + "github.com/urfave/cli/v2" // imports as package "cli" ) ... ``` +<<<<<<< HEAD **NOTE**: There is a [migrator (python) script](./cli-v1-to-v2) available to aid with the transition from the v1 to v2 API. @@ -114,20 +120,22 @@ with the transition from the v1 to v2 API. Similarly to the section above describing use of the `v2` branch, if one wants to avoid any unexpected compatibility pains once `v2` becomes `master`, then pinning to `v1` is an acceptable option, e.g.: +======= +### Using `v1` releases +>>>>>>> master ``` -$ go get gopkg.in/urfave/cli.v1 +$ go get github.com/urfave/cli ``` -``` go +```go ... import ( - "gopkg.in/urfave/cli.v1" // imports as package "cli" + "github.com/urfave/cli/v2" ) ... ``` -This will pull the latest tagged `v1` release (e.g. `v1.18.1` at the time of writing). ## Getting Started @@ -142,9 +150,10 @@ discovery. So a cli app can be as little as one line of code in `main()`. package main import ( + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -163,9 +172,10 @@ package main import ( "fmt" + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -178,7 +188,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -202,9 +215,10 @@ package main import ( "fmt" + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -217,7 +231,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -266,9 +283,10 @@ package main import ( "fmt" + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -279,7 +297,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -295,9 +316,10 @@ package main import ( "fmt" + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -323,7 +345,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -337,10 +362,11 @@ scanned. package main import ( + "log" "os" "fmt" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -369,7 +395,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -390,9 +419,10 @@ For example this: package main import ( + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -406,7 +436,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -432,9 +465,10 @@ list for the `Name`. e.g. package main import ( + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -449,7 +483,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -473,10 +510,11 @@ For example this: package main import ( + "log" "os" "sort" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func main() { @@ -515,7 +553,10 @@ func main() { sort.Sort(cli.FlagsByName(app.Flags)) sort.Sort(cli.CommandsByName(app.Commands)) - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -538,9 +579,10 @@ You can also have the default value set from the environment via `EnvVars`. e.g package main import ( + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -556,7 +598,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -571,9 +616,10 @@ resolves is used as the default. package main import ( + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -589,10 +635,52 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` +#### Values from files + +You can also have the default value set from file via `FilePath`. e.g. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "password, p", + Usage: "password for the mysql database", + FilePath: "/etc/mysql/password", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Note that default values set from file (e.g. `FilePath`) take precedence over +default values set from the environment (e.g. `EnvVar`). + #### Values from alternate input sources (YAML, TOML, and others) There is a separate package altsrc that adds support for getting flag values @@ -600,6 +688,7 @@ from other file input sources. Currently supported input source formats: * YAML +* JSON * TOML In order to get values for a flag from an alternate input source the following @@ -622,9 +711,9 @@ the yaml input source for any flags that are defined on that command. As a note the "load" flag used would also have to be defined on the command flags in order for this code snipped to work. -Currently only the aboved specified formats are supported but developers can -add support for other input sources by implementing the -altsrc.InputSourceContext for their given sources. +Currently only YAML, JSON, and TOML files are supported but developers can add support +for other input sources by implementing the altsrc.InputSourceContext for their +given sources. Here is a more complete sample of a command using YAML support: @@ -637,10 +726,11 @@ package notmain import ( "fmt" + "log" "os" - "gopkg.in/urfave/cli.v2" - "gopkg.in/urfave/cli.v2/altsrc" + "github.com/urfave/cli/v2" + "github.com/urfave/cli/v2/altsrc" ) func main() { @@ -678,7 +768,7 @@ package main import ( "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -693,7 +783,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -703,6 +796,14 @@ Will result in help output like: --port value Use a randomized port (default: random) ``` +#### Precedence + +The precedence for flag value sources is as follows (highest to lowest): + +0. Command line flag value from user +0. Environment variable (if specified) +0. Configuration file (if specified) +0. Default defined on the flag ### Subcommands @@ -717,9 +818,10 @@ package main import ( "fmt" + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -769,7 +871,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -785,9 +890,10 @@ E.g. package main import ( + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -807,7 +913,10 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -815,7 +924,7 @@ Will include: ``` COMMANDS: - noop + noop Template actions: add @@ -828,14 +937,17 @@ Calling `App.Run` will not automatically call `os.Exit`, which means that by default the exit code will "fall through" to being `0`. An explicit exit code may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a `cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: - + ``` go package main import ( + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -855,13 +967,86 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` -### Shell Completion +### Combining short options + +Traditional use of options using their shortnames look like this: + +``` +$ cmd -s -o -m "Some message" +``` + +Suppose you want users to be able to combine options with their shortnames. This +can be done using the `UseShortOptionHandling` bool in your app configuration, +or for individual commands by attaching it to the command configuration. For +example: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{} + app.UseShortOptionHandling = true + app.Commands = []*cli.Command{ + { + Name: "short", + Usage: "complete a task on the list", + Flags: []cli.Flag{ + cli.BoolFlag{Name: "serve, s"}, + cli.BoolFlag{Name: "option, o"}, + cli.StringFlag{Name: "message, m"}, + }, + Action: func(c *cli.Context) error { + fmt.Println("serve:", c.Bool("serve")) + fmt.Println("option:", c.Bool("option")) + fmt.Println("message:", c.String("message")) + return nil + }, + }, + } -You can enable completion commands by setting the `EnableShellCompletion` + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +If your program has any number of bool flags such as `serve` and `option`, and +optionally one non-bool flag `message`, with the short options of `-s`, `-o`, +and `-m` respectively, setting `UseShortOptionHandling` will also support the +following syntax: + +``` +$ cmd -som "Some message" +``` + +If you enable `UseShortOptionHandling`, then you must not use any flags that +have a single leading `-` or this will result in failures. For example, +`-option` can no longer be used. Flags with two leading dashes (such as +`--options`) are still valid. + +### Bash Completion + +You can enable completion commands by setting the `EnableBashCompletion` flag on the `App` object. By default, this setting will only auto-complete to show an app's subcommands, but you can write your own completion methods for the App or its subcommands. @@ -875,16 +1060,17 @@ package main import ( "fmt" + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} app := &cli.App{ - EnableShellCompletion: true, + EnableBashCompletion: true, Commands: []*cli.Command{ { Name: "complete", @@ -894,7 +1080,7 @@ func main() { fmt.Println("completed task: ", c.Args().First()) return nil }, - ShellComplete: func(c *cli.Context) { + BashComplete: func(c *cli.Context) { // This will complete if no args are passed if c.NArg() > 0 { return @@ -907,23 +1093,19 @@ func main() { }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` #### Enabling -You can generate bash or zsh completion code by using the flag `--init-completion bash` or `--init-completion zsh`. +Source the `autocomplete/bash_autocomplete` file in your .bashrc file while setting the `PROG` variable to the name of your program: -To setup for bash: - -``` -eval "`myprogram --init-completion bash`" -``` - -Alternatively, you can put the completion code in your `.bashrc` file: ``` -myprogram --init-completion bash >> ~/.bashrc +PROG=myprogram source /.../cli/autocomplete/bash_autocomplete ``` #### Distribution @@ -955,9 +1137,10 @@ The default shell completion flag (`--generate-completion`) is defined as package main import ( + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -974,7 +1157,10 @@ func main() { }, }, } - app.Run(os.Args) + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } } ``` @@ -1000,10 +1186,11 @@ package main import ( "fmt" + "log" "io" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -1042,7 +1229,7 @@ VERSION: cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { fmt.Println("Ha HA. I pwnd the help!!1") } - + (&cli.App{}).Run(os.Args) } ``` @@ -1058,9 +1245,10 @@ setting `cli.HelpFlag`, e.g.: package main import ( + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -1069,7 +1257,7 @@ func main() { Usage: "HALP", EnvVars: []string{"SHOW_HALP", "HALPPLZ"}, } - + (&cli.App{}).Run(os.Args) } ``` @@ -1093,9 +1281,10 @@ setting `cli.VersionFlag`, e.g.: package main import ( + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func main() { @@ -1103,7 +1292,7 @@ func main() { Name: "print-version", Aliases: []string{"V"}, Usage: "print only the version", } - + app := &cli.App{ Name: "partay", Version: "v19.99.0", @@ -1123,9 +1312,10 @@ package main import ( "fmt" + "log" "os" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) var ( @@ -1136,7 +1326,7 @@ func main() { cli.VersionPrinter = func(c *cli.Context) { fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision) } - + app := &cli.App{ Name: "partay", Version: "v19.99.0", @@ -1165,7 +1355,7 @@ import ( "os" "time" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func init() { @@ -1217,7 +1407,7 @@ func (g *genericType) String() string { } func main() { - app := cli.App{ + app := &cli.App{ Name: "kənˈtrīv", Version: "v19.99.0", Compiled: time.Now(), @@ -1406,7 +1596,7 @@ func main() { app.Writer = &hexWriter{} app.ErrWriter = &hexWriter{} } - + app.Run(os.Args) } @@ -1418,16 +1608,4 @@ func wopAction(c *cli.Context) error { ## Contribution Guidelines -Feel free to put up a pull request to fix a bug or maybe add a feature. I will -give it a code review and make sure that it does not break backwards -compatibility. If I or any other collaborators agree that it is in line with -the vision of the project, we will work with you to get the code into -a mergeable state and merge it into the master branch. - -If you have contributed something significant to the project, we will most -likely add you as a collaborator. As a collaborator you are given the ability -to merge others pull requests. It is very important that new code does not -break existing code, so be careful about what code you do choose to merge. - -If you feel like you have contributed to the project but have not yet been -added as a collaborator, we probably forgot to add you, please open an issue. +See [./CONTRIBUTING.md](./CONTRIBUTING.md) diff --git a/altsrc/altsrc.go b/altsrc/altsrc.go deleted file mode 100644 index ac34bf6..0000000 --- a/altsrc/altsrc.go +++ /dev/null @@ -1,3 +0,0 @@ -package altsrc - -//go:generate python ../generate-flag-types altsrc -i ../flag-types.json -o flag_generated.go diff --git a/altsrc/flag.go b/altsrc/flag.go index 858e54e..31b8a04 100644 --- a/altsrc/flag.go +++ b/altsrc/flag.go @@ -6,7 +6,7 @@ import ( "strconv" "syscall" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) // FlagInputSourceExtension is an extension interface of cli.Flag that @@ -72,7 +72,7 @@ func (f *GenericFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourc } if value != nil { for _, name := range f.Names() { - f.set.Set(name, value.String()) + _ = f.set.Set(name, value.String()) } } } @@ -135,7 +135,7 @@ func (f *BoolFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo } if value { for _, name := range f.Names() { - f.set.Set(name, strconv.FormatBool(value)) + _ = f.set.Set(name, strconv.FormatBool(value)) } } } @@ -153,7 +153,7 @@ func (f *StringFlag) ApplyInputSourceValue(context *cli.Context, isc InputSource } if value != "" { for _, name := range f.Names() { - f.set.Set(name, value) + _ = f.set.Set(name, value) } } } @@ -181,7 +181,7 @@ func (f *PathFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCo value = filepath.Join(filepath.Dir(basePathAbs), value) } - f.set.Set(name, value) + _ = f.set.Set(name, value) } } } @@ -199,7 +199,7 @@ func (f *IntFlag) ApplyInputSourceValue(context *cli.Context, isc InputSourceCon } if value > 0 { for _, name := range f.Names() { - f.set.Set(name, strconv.FormatInt(int64(value), 10)) + _ = f.set.Set(name, strconv.FormatInt(int64(value), 10)) } } } @@ -217,7 +217,7 @@ func (f *DurationFlag) ApplyInputSourceValue(context *cli.Context, isc InputSour } if value > 0 { for _, name := range f.Names() { - f.set.Set(name, value.String()) + _ = f.set.Set(name, value.String()) } } } @@ -236,7 +236,7 @@ func (f *Float64Flag) ApplyInputSourceValue(context *cli.Context, isc InputSourc if value > 0 { floatStr := float64ToString(value) for _, name := range f.Names() { - f.set.Set(name, floatStr) + _ = f.set.Set(name, floatStr) } } } diff --git a/altsrc/flag_generated.go b/altsrc/flag_generated.go index 87c7c5d..de3e2b5 100644 --- a/altsrc/flag_generated.go +++ b/altsrc/flag_generated.go @@ -2,12 +2,9 @@ package altsrc import ( "flag" - - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) -// WARNING: This file is generated! - // BoolFlag is the flag type that wraps cli.BoolFlag to allow // for other values to be specified type BoolFlag struct { @@ -20,18 +17,11 @@ func NewBoolFlag(fl *cli.BoolFlag) *BoolFlag { return &BoolFlag{BoolFlag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped BoolFlag.Apply -func (f *BoolFlag) Apply(set *flag.FlagSet) { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped BoolFlag.Apply +func (f *BoolFlag) Apply(set *flag.FlagSet) error { f.set = set - f.BoolFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped BoolFlag.ApplyWithError -func (f *BoolFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.BoolFlag.ApplyWithError(set) + return f.BoolFlag.Apply(set) } // DurationFlag is the flag type that wraps cli.DurationFlag to allow @@ -46,18 +36,11 @@ func NewDurationFlag(fl *cli.DurationFlag) *DurationFlag { return &DurationFlag{DurationFlag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped DurationFlag.Apply -func (f *DurationFlag) Apply(set *flag.FlagSet) { - f.set = set - f.DurationFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped DurationFlag.ApplyWithError -func (f *DurationFlag) ApplyWithError(set *flag.FlagSet) error { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped DurationFlag.Apply +func (f *DurationFlag) Apply(set *flag.FlagSet) error { f.set = set - return f.DurationFlag.ApplyWithError(set) + return f.DurationFlag.Apply(set) } // Float64Flag is the flag type that wraps cli.Float64Flag to allow @@ -72,18 +55,11 @@ func NewFloat64Flag(fl *cli.Float64Flag) *Float64Flag { return &Float64Flag{Float64Flag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped Float64Flag.Apply -func (f *Float64Flag) Apply(set *flag.FlagSet) { - f.set = set - f.Float64Flag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped Float64Flag.ApplyWithError -func (f *Float64Flag) ApplyWithError(set *flag.FlagSet) error { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped Float64Flag.Apply +func (f *Float64Flag) Apply(set *flag.FlagSet) error { f.set = set - return f.Float64Flag.ApplyWithError(set) + return f.Float64Flag.Apply(set) } // GenericFlag is the flag type that wraps cli.GenericFlag to allow @@ -98,18 +74,11 @@ func NewGenericFlag(fl *cli.GenericFlag) *GenericFlag { return &GenericFlag{GenericFlag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped GenericFlag.Apply -func (f *GenericFlag) Apply(set *flag.FlagSet) { - f.set = set - f.GenericFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped GenericFlag.ApplyWithError -func (f *GenericFlag) ApplyWithError(set *flag.FlagSet) error { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped GenericFlag.Apply +func (f *GenericFlag) Apply(set *flag.FlagSet) error { f.set = set - return f.GenericFlag.ApplyWithError(set) + return f.GenericFlag.Apply(set) } // Int64Flag is the flag type that wraps cli.Int64Flag to allow @@ -124,18 +93,11 @@ func NewInt64Flag(fl *cli.Int64Flag) *Int64Flag { return &Int64Flag{Int64Flag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped Int64Flag.Apply -func (f *Int64Flag) Apply(set *flag.FlagSet) { - f.set = set - f.Int64Flag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped Int64Flag.ApplyWithError -func (f *Int64Flag) ApplyWithError(set *flag.FlagSet) error { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped Int64Flag.Apply +func (f *Int64Flag) Apply(set *flag.FlagSet) error { f.set = set - return f.Int64Flag.ApplyWithError(set) + return f.Int64Flag.Apply(set) } // IntFlag is the flag type that wraps cli.IntFlag to allow @@ -150,18 +112,11 @@ func NewIntFlag(fl *cli.IntFlag) *IntFlag { return &IntFlag{IntFlag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped IntFlag.Apply -func (f *IntFlag) Apply(set *flag.FlagSet) { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped IntFlag.Apply +func (f *IntFlag) Apply(set *flag.FlagSet) error { f.set = set - f.IntFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped IntFlag.ApplyWithError -func (f *IntFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.IntFlag.ApplyWithError(set) + return f.IntFlag.Apply(set) } // IntSliceFlag is the flag type that wraps cli.IntSliceFlag to allow @@ -176,18 +131,11 @@ func NewIntSliceFlag(fl *cli.IntSliceFlag) *IntSliceFlag { return &IntSliceFlag{IntSliceFlag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped IntSliceFlag.Apply -func (f *IntSliceFlag) Apply(set *flag.FlagSet) { - f.set = set - f.IntSliceFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped IntSliceFlag.ApplyWithError -func (f *IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped IntSliceFlag.Apply +func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { f.set = set - return f.IntSliceFlag.ApplyWithError(set) + return f.IntSliceFlag.Apply(set) } // Int64SliceFlag is the flag type that wraps cli.Int64SliceFlag to allow @@ -202,18 +150,11 @@ func NewInt64SliceFlag(fl *cli.Int64SliceFlag) *Int64SliceFlag { return &Int64SliceFlag{Int64SliceFlag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped Int64SliceFlag.Apply -func (f *Int64SliceFlag) Apply(set *flag.FlagSet) { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped Int64SliceFlag.Apply +func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { f.set = set - f.Int64SliceFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped Int64SliceFlag.ApplyWithError -func (f *Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.Int64SliceFlag.ApplyWithError(set) + return f.Int64SliceFlag.Apply(set) } // Float64SliceFlag is the flag type that wraps cli.Float64SliceFlag to allow @@ -230,16 +171,9 @@ func NewFloat64SliceFlag(fl *cli.Float64SliceFlag) *Float64SliceFlag { // Apply saves the flagSet for later usage calls, then calls the // wrapped Float64SliceFlag.Apply -func (f *Float64SliceFlag) Apply(set *flag.FlagSet) { - f.set = set - f.Float64SliceFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped Float64SliceFlag.ApplyWithError -func (f *Float64SliceFlag) ApplyWithError(set *flag.FlagSet) error { +func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { f.set = set - return f.Float64SliceFlag.ApplyWithError(set) + return f.Float64SliceFlag.Apply(set) } // StringFlag is the flag type that wraps cli.StringFlag to allow @@ -254,18 +188,11 @@ func NewStringFlag(fl *cli.StringFlag) *StringFlag { return &StringFlag{StringFlag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped StringFlag.Apply -func (f *StringFlag) Apply(set *flag.FlagSet) { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped StringFlag.Apply +func (f *StringFlag) Apply(set *flag.FlagSet) error { f.set = set - f.StringFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped StringFlag.ApplyWithError -func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.StringFlag.ApplyWithError(set) + return f.StringFlag.Apply(set) } // PathFlag is the flag type that wraps cli.PathFlag to allow @@ -282,16 +209,9 @@ func NewPathFlag(fl *cli.PathFlag) *PathFlag { // Apply saves the flagSet for later usage calls, then calls the // wrapped PathFlag.Apply -func (f *PathFlag) Apply(set *flag.FlagSet) { - f.set = set - f.PathFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped PathFlag.ApplyWithError -func (f *PathFlag) ApplyWithError(set *flag.FlagSet) error { +func (f *PathFlag) Apply(set *flag.FlagSet) error { f.set = set - return f.PathFlag.ApplyWithError(set) + return f.PathFlag.Apply(set) } // StringSliceFlag is the flag type that wraps cli.StringSliceFlag to allow @@ -306,18 +226,11 @@ func NewStringSliceFlag(fl *cli.StringSliceFlag) *StringSliceFlag { return &StringSliceFlag{StringSliceFlag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped StringSliceFlag.Apply -func (f *StringSliceFlag) Apply(set *flag.FlagSet) { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped StringSliceFlag.Apply +func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { f.set = set - f.StringSliceFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped StringSliceFlag.ApplyWithError -func (f *StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { - f.set = set - return f.StringSliceFlag.ApplyWithError(set) + return f.StringSliceFlag.Apply(set) } // Uint64Flag is the flag type that wraps cli.Uint64Flag to allow @@ -332,18 +245,11 @@ func NewUint64Flag(fl *cli.Uint64Flag) *Uint64Flag { return &Uint64Flag{Uint64Flag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped Uint64Flag.Apply -func (f *Uint64Flag) Apply(set *flag.FlagSet) { - f.set = set - f.Uint64Flag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped Uint64Flag.ApplyWithError -func (f *Uint64Flag) ApplyWithError(set *flag.FlagSet) error { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped Uint64Flag.Apply +func (f *Uint64Flag) Apply(set *flag.FlagSet) error { f.set = set - return f.Uint64Flag.ApplyWithError(set) + return f.Uint64Flag.Apply(set) } // UintFlag is the flag type that wraps cli.UintFlag to allow @@ -358,16 +264,9 @@ func NewUintFlag(fl *cli.UintFlag) *UintFlag { return &UintFlag{UintFlag: fl, set: nil} } -// Apply saves the flagSet for later usage calls, then calls the -// wrapped UintFlag.Apply -func (f *UintFlag) Apply(set *flag.FlagSet) { - f.set = set - f.UintFlag.Apply(set) -} - -// ApplyWithError saves the flagSet for later usage calls, then calls the -// wrapped UintFlag.ApplyWithError -func (f *UintFlag) ApplyWithError(set *flag.FlagSet) error { +// Apply saves the flagSet for later usage calls, then calls +// the wrapped UintFlag.Apply +func (f *UintFlag) Apply(set *flag.FlagSet) error { f.set = set - return f.UintFlag.ApplyWithError(set) + return f.UintFlag.Apply(set) } diff --git a/altsrc/flag_test.go b/altsrc/flag_test.go index 087e607..b5fcbf5 100644 --- a/altsrc/flag_test.go +++ b/altsrc/flag_test.go @@ -3,13 +3,12 @@ package altsrc import ( "flag" "fmt" + "github.com/urfave/cli/v2" "os" "runtime" "strings" "testing" "time" - - "gopkg.in/urfave/cli.v2" ) type testApplyInputSource struct { @@ -252,30 +251,30 @@ func TestDurationApplyInputSourceMethodSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}), FlagName: "test", - MapValue: time.Duration(30 * time.Second), + MapValue: 30 * time.Second, }) - expect(t, time.Duration(30*time.Second), c.Duration("test")) + expect(t, 30*time.Second, c.Duration("test")) } func TestDurationApplyInputSourceMethodContextSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewDurationFlag(&cli.DurationFlag{Name: "test"}), FlagName: "test", - MapValue: time.Duration(30 * time.Second), - ContextValueString: time.Duration(15 * time.Second).String(), + MapValue: 30 * time.Second, + ContextValueString: (15 * time.Second).String(), }) - expect(t, time.Duration(15*time.Second), c.Duration("test")) + expect(t, 15*time.Second, c.Duration("test")) } func TestDurationApplyInputSourceMethodEnvVarSet(t *testing.T) { c := runTest(t, testApplyInputSource{ Flag: NewDurationFlag(&cli.DurationFlag{Name: "test", EnvVars: []string{"TEST"}}), FlagName: "test", - MapValue: time.Duration(30 * time.Second), + MapValue: 30 * time.Second, EnvVarName: "TEST", - EnvVarValue: time.Duration(15 * time.Second).String(), + EnvVarValue: (15 * time.Second).String(), }) - expect(t, time.Duration(15*time.Second), c.Duration("test")) + expect(t, 15*time.Second, c.Duration("test")) } func TestFloat64ApplyInputSourceMethodSet(t *testing.T) { @@ -316,19 +315,19 @@ func runTest(t *testing.T, test testApplyInputSource) *cli.Context { set := flag.NewFlagSet(test.FlagSetName, flag.ContinueOnError) c := cli.NewContext(nil, set, nil) if test.EnvVarName != "" && test.EnvVarValue != "" { - os.Setenv(test.EnvVarName, test.EnvVarValue) + _ = os.Setenv(test.EnvVarName, test.EnvVarValue) defer os.Setenv(test.EnvVarName, "") } test.Flag.Apply(set) if test.ContextValue != nil { - flag := set.Lookup(test.FlagName) - flag.Value = test.ContextValue + f := set.Lookup(test.FlagName) + f.Value = test.ContextValue } if test.ContextValueString != "" { - set.Set(test.FlagName, test.ContextValueString) + _ = set.Set(test.FlagName, test.ContextValueString) } - test.Flag.ApplyInputSourceValue(c, inputSource) + _ = test.Flag.ApplyInputSourceValue(c, inputSource) return c } diff --git a/altsrc/input_source_context.go b/altsrc/input_source_context.go index bb0afdb..5f60574 100644 --- a/altsrc/input_source_context.go +++ b/altsrc/input_source_context.go @@ -1,9 +1,8 @@ package altsrc import ( + "github.com/urfave/cli/v2" "time" - - "gopkg.in/urfave/cli.v2" ) // InputSourceContext is an interface used to allow diff --git a/altsrc/json_command_test.go b/altsrc/json_command_test.go index ccb5041..bd8e6cf 100644 --- a/altsrc/json_command_test.go +++ b/altsrc/json_command_test.go @@ -2,11 +2,10 @@ package altsrc import ( "flag" + "github.com/urfave/cli/v2" "io/ioutil" "os" "testing" - - "gopkg.in/urfave/cli.v2" ) const ( @@ -22,7 +21,7 @@ func TestCommandJSONFileTest(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) test := []string{"test-cmd", "--load", fileName} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -52,11 +51,11 @@ func TestCommandJSONFileTestGlobalEnvVarWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - os.Setenv("THE_TEST", "10") + _ = os.Setenv("THE_TEST", "10") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", fileName} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -87,11 +86,11 @@ func TestCommandJSONFileTestGlobalEnvVarWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - os.Setenv("THE_TEST", "10") + _ = os.Setenv("THE_TEST", "10") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", fileName} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -123,7 +122,7 @@ func TestCommandJSONFileTestSpecifiedFlagWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) test := []string{"test-cmd", "--load", fileName, "--test", "7"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -155,7 +154,7 @@ func TestCommandJSONFileTestSpecifiedFlagWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) test := []string{"test-cmd", "--load", fileName, "--top.test", "7"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -187,7 +186,7 @@ func TestCommandJSONFileTestDefaultValueFileWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) test := []string{"test-cmd", "--load", fileName} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -219,7 +218,7 @@ func TestCommandJSONFileTestDefaultValueFileWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) test := []string{"test-cmd", "--load", fileName} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -250,11 +249,11 @@ func TestCommandJSONFileFlagHasDefaultGlobalEnvJSONSetGlobalEnvWins(t *testing.T app := &cli.App{} set := flag.NewFlagSet("test", 0) - os.Setenv("THE_TEST", "11") + _ = os.Setenv("THE_TEST", "11") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", fileName} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -284,11 +283,11 @@ func TestCommandJSONFileFlagHasDefaultGlobalEnvJSONSetGlobalEnvWinsNested(t *tes app := &cli.App{} set := flag.NewFlagSet("test", 0) - os.Setenv("THE_TEST", "11") + _ = os.Setenv("THE_TEST", "11") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", fileName} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) diff --git a/altsrc/json_source_context.go b/altsrc/json_source_context.go index 34c7eb9..41602ad 100644 --- a/altsrc/json_source_context.go +++ b/altsrc/json_source_context.go @@ -3,12 +3,11 @@ package altsrc import ( "encoding/json" "fmt" + "github.com/urfave/cli/v2" "io" "io/ioutil" "strings" "time" - - "gopkg.in/urfave/cli.v2" ) // NewJSONSourceFromFlagFunc returns a func that takes a cli.Context @@ -29,13 +28,8 @@ func NewJSONSourceFromFile(f string) (InputSourceContext, error) { if err != nil { return nil, err } - s, err := newJSONSource(data) - if err != nil { - return nil, err - } - s.file = f - return s, nil + return NewJSONSource(data) } // NewJSONSourceFromReader returns an InputSourceContext suitable for @@ -51,10 +45,6 @@ func NewJSONSourceFromReader(r io.Reader) (InputSourceContext, error) { // NewJSONSource returns an InputSourceContext suitable for retrieving // config variables from raw JSON data. func NewJSONSource(data []byte) (InputSourceContext, error) { - return newJSONSource(data) -} - -func newJSONSource(data []byte) (*jsonSource, error) { var deserialized map[string]interface{} if err := json.Unmarshal(data, &deserialized); err != nil { return nil, err @@ -77,9 +67,9 @@ func (x *jsonSource) Int(name string) (int, error) { case int: return v, nil case float64: - return int(float64(v)), nil + return int(v), nil case float32: - return int(float32(v)), nil + return int(v), nil } } diff --git a/altsrc/map_input_source.go b/altsrc/map_input_source.go index 41221b1..66bd6e8 100644 --- a/altsrc/map_input_source.go +++ b/altsrc/map_input_source.go @@ -2,11 +2,10 @@ package altsrc import ( "fmt" + "github.com/urfave/cli/v2" "reflect" "strings" "time" - - "gopkg.in/urfave/cli.v2" ) // MapInputSource implements InputSourceContext to return @@ -23,15 +22,15 @@ func nestedVal(name string, tree map[interface{}]interface{}) (interface{}, bool if sections := strings.Split(name, "."); len(sections) > 1 { node := tree for _, section := range sections[:len(sections)-1] { - if child, ok := node[section]; !ok { + child, ok := node[section] + if !ok { + return nil, false + } + ctype, ok := child.(map[interface{}]interface{}) + if !ok { return nil, false - } else { - if ctype, ok := child.(map[interface{}]interface{}); !ok { - return nil, false - } else { - node = ctype - } } + node = ctype } if val, ok := node[sections[len(sections)-1]]; ok { return val, true diff --git a/altsrc/toml_command_test.go b/altsrc/toml_command_test.go index 2f28d22..84558aa 100644 --- a/altsrc/toml_command_test.go +++ b/altsrc/toml_command_test.go @@ -7,20 +7,19 @@ package altsrc import ( "flag" + "github.com/urfave/cli/v2" "io/ioutil" "os" "testing" - - "gopkg.in/urfave/cli.v2" ) func TestCommandTomFileTest(t *testing.T) { - app := (&cli.App{}) + app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) + _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) defer os.Remove("current.toml") test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -45,15 +44,15 @@ func TestCommandTomFileTest(t *testing.T) { } func TestCommandTomlFileTestGlobalEnvVarWins(t *testing.T) { - app := (&cli.App{}) + app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) + _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) defer os.Remove("current.toml") - os.Setenv("THE_TEST", "10") + _ = os.Setenv("THE_TEST", "10") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -79,15 +78,15 @@ func TestCommandTomlFileTestGlobalEnvVarWins(t *testing.T) { } func TestCommandTomlFileTestGlobalEnvVarWinsNested(t *testing.T) { - app := (&cli.App{}) + app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) + _ = ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) defer os.Remove("current.toml") - os.Setenv("THE_TEST", "10") + _ = os.Setenv("THE_TEST", "10") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -113,13 +112,13 @@ func TestCommandTomlFileTestGlobalEnvVarWinsNested(t *testing.T) { } func TestCommandTomlFileTestSpecifiedFlagWins(t *testing.T) { - app := (&cli.App{}) + app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) + _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) defer os.Remove("current.toml") test := []string{"test-cmd", "--load", "current.toml", "--test", "7"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -145,14 +144,14 @@ func TestCommandTomlFileTestSpecifiedFlagWins(t *testing.T) { } func TestCommandTomlFileTestSpecifiedFlagWinsNested(t *testing.T) { - app := (&cli.App{}) + app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte(`[top] + _ = ioutil.WriteFile("current.toml", []byte(`[top] test = 15`), 0666) defer os.Remove("current.toml") test := []string{"test-cmd", "--load", "current.toml", "--top.test", "7"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -178,13 +177,13 @@ func TestCommandTomlFileTestSpecifiedFlagWinsNested(t *testing.T) { } func TestCommandTomlFileTestDefaultValueFileWins(t *testing.T) { - app := (&cli.App{}) + app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) + _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) defer os.Remove("current.toml") test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -210,13 +209,13 @@ func TestCommandTomlFileTestDefaultValueFileWins(t *testing.T) { } func TestCommandTomlFileTestDefaultValueFileWinsNested(t *testing.T) { - app := (&cli.App{}) + app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) + _ = ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) defer os.Remove("current.toml") test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -244,14 +243,14 @@ func TestCommandTomlFileTestDefaultValueFileWinsNested(t *testing.T) { func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWins(t *testing.T) { app := (&cli.App{}) set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) + _ = ioutil.WriteFile("current.toml", []byte("test = 15"), 0666) defer os.Remove("current.toml") - os.Setenv("THE_TEST", "11") + _ = os.Setenv("THE_TEST", "11") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -276,16 +275,16 @@ func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWins(t *testing.T } func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWinsNested(t *testing.T) { - app := (&cli.App{}) + app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) + _ = ioutil.WriteFile("current.toml", []byte("[top]\ntest = 15"), 0666) defer os.Remove("current.toml") - os.Setenv("THE_TEST", "11") + _ = os.Setenv("THE_TEST", "11") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", "current.toml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) diff --git a/altsrc/toml_file_loader.go b/altsrc/toml_file_loader.go index 1cb2d7b..3f410fc 100644 --- a/altsrc/toml_file_loader.go +++ b/altsrc/toml_file_loader.go @@ -10,7 +10,7 @@ import ( "reflect" "github.com/BurntSushi/toml" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) type tomlMap struct { @@ -28,7 +28,7 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) { case reflect.String: ret[key] = val.(string) case reflect.Int: - ret[key] = int(val.(int)) + ret[key] = val.(int) case reflect.Int8: ret[key] = int(val.(int8)) case reflect.Int16: @@ -50,7 +50,7 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) { case reflect.Float32: ret[key] = float64(val.(float32)) case reflect.Float64: - ret[key] = float64(val.(float64)) + ret[key] = val.(float64) case reflect.Map: if tmp, err := unmarshalMap(val); err == nil { ret[key] = tmp @@ -66,9 +66,9 @@ func unmarshalMap(i interface{}) (ret map[interface{}]interface{}, err error) { return ret, nil } -func (self *tomlMap) UnmarshalTOML(i interface{}) error { +func (tm *tomlMap) UnmarshalTOML(i interface{}) error { if tmp, err := unmarshalMap(i); err == nil { - self.Map = tmp + tm.Map = tmp } else { return err } diff --git a/altsrc/yaml_command_test.go b/altsrc/yaml_command_test.go index 5290e84..2190c2b 100644 --- a/altsrc/yaml_command_test.go +++ b/altsrc/yaml_command_test.go @@ -10,17 +10,16 @@ import ( "io/ioutil" "os" "testing" - - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" ) func TestCommandYamlFileTest(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) + _ = ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) defer os.Remove("current.yaml") test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -47,13 +46,13 @@ func TestCommandYamlFileTest(t *testing.T) { func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) + _ = ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) defer os.Remove("current.yaml") - os.Setenv("THE_TEST", "10") + _ = os.Setenv("THE_TEST", "10") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -81,14 +80,14 @@ func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { func TestCommandYamlFileTestGlobalEnvVarWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte(`top: + _ = ioutil.WriteFile("current.yaml", []byte(`top: test: 15`), 0666) defer os.Remove("current.yaml") - os.Setenv("THE_TEST", "10") + _ = os.Setenv("THE_TEST", "10") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -116,11 +115,11 @@ func TestCommandYamlFileTestGlobalEnvVarWinsNested(t *testing.T) { func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) + _ = ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) defer os.Remove("current.yaml") test := []string{"test-cmd", "--load", "current.yaml", "--test", "7"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -148,12 +147,12 @@ func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { func TestCommandYamlFileTestSpecifiedFlagWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte(`top: + _ = ioutil.WriteFile("current.yaml", []byte(`top: test: 15`), 0666) defer os.Remove("current.yaml") test := []string{"test-cmd", "--load", "current.yaml", "--top.test", "7"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -181,11 +180,11 @@ func TestCommandYamlFileTestSpecifiedFlagWinsNested(t *testing.T) { func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) + _ = ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) defer os.Remove("current.yaml") test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -213,12 +212,12 @@ func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { func TestCommandYamlFileTestDefaultValueFileWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte(`top: + _ = ioutil.WriteFile("current.yaml", []byte(`top: test: 15`), 0666) defer os.Remove("current.yaml") test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -246,14 +245,14 @@ func TestCommandYamlFileTestDefaultValueFileWinsNested(t *testing.T) { func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) + _ = ioutil.WriteFile("current.yaml", []byte("test: 15"), 0666) defer os.Remove("current.yaml") - os.Setenv("THE_TEST", "11") + _ = os.Setenv("THE_TEST", "11") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) @@ -280,15 +279,15 @@ func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWinsNested(t *testing.T) { app := &cli.App{} set := flag.NewFlagSet("test", 0) - ioutil.WriteFile("current.yaml", []byte(`top: + _ = ioutil.WriteFile("current.yaml", []byte(`top: test: 15`), 0666) defer os.Remove("current.yaml") - os.Setenv("THE_TEST", "11") + _ = os.Setenv("THE_TEST", "11") defer os.Setenv("THE_TEST", "") test := []string{"test-cmd", "--load", "current.yaml"} - set.Parse(test) + _ = set.Parse(test) c := cli.NewContext(app, set, nil) diff --git a/altsrc/yaml_file_loader.go b/altsrc/yaml_file_loader.go index 37c8d9c..9b1dc05 100644 --- a/altsrc/yaml_file_loader.go +++ b/altsrc/yaml_file_loader.go @@ -14,7 +14,7 @@ import ( "runtime" "strings" - "gopkg.in/urfave/cli.v2" + "github.com/urfave/cli/v2" "gopkg.in/yaml.v2" ) @@ -86,7 +86,7 @@ func loadDataFrom(filePath string) ([]byte, error) { return nil, fmt.Errorf("Cannot read from file: '%s' because it does not exist.", filePath) } return ioutil.ReadFile(filePath) - } else { - return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) } + + return nil, fmt.Errorf("unable to determine how to load from path %s", filePath) } diff --git a/app.go b/app.go index 4c073c5..229254b 100644 --- a/app.go +++ b/app.go @@ -1,9 +1,9 @@ package cli import ( + "flag" "fmt" "io" - "io/ioutil" "os" "path/filepath" "reflect" @@ -11,7 +11,21 @@ import ( "time" ) -// App is the main structure of a cli application. +var ( + changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md" + appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) + // unused variable. commented for now. will remove in future if agreed upon by everyone + //runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL) + + contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." + + errInvalidActionType = NewExitError("ERROR invalid Action type. "+ + fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+ + fmt.Sprintf("See %s", appActionDeprecationURL), 2) +) + +// App is the main structure of a cli application. It is recommended that +// an app be created with the cli.NewApp() function type App struct { // The name of the program. Defaults to path.Base(os.Args[0]) Name string @@ -31,8 +45,8 @@ type App struct { Commands []*Command // List of flags to parse Flags []Flag - // Boolean to enable shell completion commands - EnableShellCompletion bool + // Boolean to enable bash completion commands + EnableBashCompletion bool // Boolean to hide built-in help command HideHelp bool // Boolean to hide built-in version flag and the VERSION section of help @@ -40,7 +54,7 @@ type App struct { // Categories contains the categorized commands and is populated on app startup Categories CommandCategories // An action to execute when the shell completion flag is set - ShellComplete ShellCompleteFunc + BashComplete BashCompleteFunc // An action to execute before any subcommands are run, but after the context is ready // If a non-nil error is returned, no subcommands are run Before BeforeFunc @@ -63,6 +77,9 @@ type App struct { Writer io.Writer // ErrWriter writes error output ErrWriter io.Writer + // Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to + // function as a default, so this is optional. + ExitErrHandler ExitErrHandlerFunc // Other custom info Metadata map[string]interface{} // Carries a function which returns app specific info. @@ -71,6 +88,10 @@ type App struct { // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomAppHelpTemplate string + // Boolean to enable short-option handling so user can combine several + // single-character bool arguements into one + // i.e. foobar -o -v -> foobar -ov + UseShortOptionHandling bool didSetup bool } @@ -85,6 +106,22 @@ func compileTime() time.Time { return info.ModTime() } +// NewApp creates a new cli Application with some reasonable defaults for Name, +// Usage, Version and Action. +func NewApp() *App { + return &App{ + Name: filepath.Base(os.Args[0]), + HelpName: filepath.Base(os.Args[0]), + Usage: "A new cli application", + UsageText: "", + Version: "0.0.0", + BashComplete: DefaultAppComplete, + Action: helpCommand.Action, + Compiled: compileTime(), + Writer: os.Stdout, + } +} + // Setup runs initialization code to ensure all data structures are ready for // `Run` or inspection prior to `Run`. It is internally called by `Run`, but // will return early if setup has already happened. @@ -111,8 +148,8 @@ func (a *App) Setup() { a.Version = "0.0.0" } - if a.ShellComplete == nil { - a.ShellComplete = DefaultAppComplete + if a.BashComplete == nil { + a.BashComplete = DefaultAppComplete } if a.Action == nil { @@ -127,14 +164,15 @@ func (a *App) Setup() { a.Writer = os.Stdout } - newCmds := []*Command{} + var newCommands []*Command + for _, c := range a.Commands { if c.HelpName == "" { c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) } - newCmds = append(newCmds, c) + newCommands = append(newCommands, c) } - a.Commands = newCmds + a.Commands = newCommands if a.Command(helpCommand.Name) == nil && !a.HideHelp { a.appendCommand(helpCommand) @@ -144,10 +182,10 @@ func (a *App) Setup() { } } - if a.EnableShellCompletion { - a.appendFlag(GenerateCompletionFlag) - a.appendFlag(InitCompletionFlag) - } + //if a.EnableShellCompletion { + // a.appendFlag(GenerateCompletionFlag) + // a.appendFlag(InitCompletionFlag) + //} if !a.HideVersion { a.appendFlag(VersionFlag) @@ -168,6 +206,14 @@ func (a *App) Setup() { } } +func (a *App) newFlagSet() (*flag.FlagSet, error) { + return flagSet(a.Name, a.Flags) +} + +func (a *App) useShortOptionHandling() bool { + return a.UseShortOptionHandling +} + // Run is the entry point to the cli app. Parses the arguments slice and routes // to the proper flag/args combination func (a *App) Run(arguments []string) (err error) { @@ -181,19 +227,17 @@ func (a *App) Run(arguments []string) (err error) { // always appends the completion flag at the end of the command shellComplete, arguments := checkShellCompleteFlag(a, arguments) - // parse flags - set, err := flagSet(a.Name, a.Flags) + _, err = a.newFlagSet() if err != nil { return err } - set.SetOutput(ioutil.Discard) - err = set.Parse(arguments[1:]) + set, err := parseIter(a, arguments[1:]) nerr := normalizeFlags(a.Flags, set) context := NewContext(a, set, nil) if nerr != nil { - fmt.Fprintln(a.Writer, nerr) - ShowAppHelp(context) + _, _ = fmt.Fprintln(a.Writer, nerr) + _ = ShowAppHelp(context) return nerr } context.shellComplete = shellComplete @@ -212,17 +256,22 @@ func (a *App) Run(arguments []string) (err error) { if err != nil { if a.OnUsageError != nil { + //<<<<<<< HEAD err = a.OnUsageError(context, err, false) HandleExitCoder(err) + //======= + // err := a.OnUsageError(context, err, false) + // a.handleExitCoder(context, err) + //>>>>>>> master return err } - fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) - ShowAppHelp(context) + _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) + _ = ShowAppHelp(context) return err } if !a.HideHelp && checkHelp(context) { - ShowAppHelp(context) + _ = ShowAppHelp(context) return nil } @@ -231,6 +280,12 @@ func (a *App) Run(arguments []string) (err error) { return nil } + cerr := checkRequiredFlags(a.Flags, context) + if cerr != nil { + _ = ShowAppHelp(context) + return cerr + } + if a.After != nil { defer func() { if afterErr := a.After(context); afterErr != nil { @@ -246,8 +301,9 @@ func (a *App) Run(arguments []string) (err error) { if a.Before != nil { beforeErr := a.Before(context) if beforeErr != nil { - ShowAppHelp(context) - HandleExitCoder(beforeErr) + _, _ = fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) + _ = ShowAppHelp(context) + a.handleExitCoder(context, beforeErr) err = beforeErr return err } @@ -269,10 +325,22 @@ func (a *App) Run(arguments []string) (err error) { // Run default Action err = a.Action(context) - HandleExitCoder(err) + a.handleExitCoder(context, err) return err } +// RunAndExitOnError calls .Run() and exits non-zero if an error was returned +// +// Deprecated: instead you should return an error that fulfills cli.ExitCoder +// to cli.App.Run. This will cause the application to exit with the given eror +// code in the cli.ExitCoder +func (a *App) RunAndExitOnError() { + if err := a.Run(os.Args); err != nil { + _, _ = fmt.Fprintln(a.errWriter(), err) + OsExiter(1) + } +} + // RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to // generate command-specific flags func (a *App) RunAsSubcommand(ctx *Context) (err error) { @@ -298,29 +366,32 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } a.Commands = newCmds - // append flags - if a.EnableShellCompletion { - a.appendFlag(GenerateCompletionFlag) - } - - // parse flags - set, err := flagSet(a.Name, a.Flags) + //<<<<<<< HEAD + // // append flags + // if a.EnableShellCompletion { + // a.appendFlag(GenerateCompletionFlag) + // } + // + // // parse flags + // set, err := flagSet(a.Name, a.Flags) + //======= + _, err = a.newFlagSet() + //>>>>>>> master if err != nil { return err } - set.SetOutput(ioutil.Discard) - err = set.Parse(ctx.Args().Tail()) + set, err := parseIter(a, ctx.Args().Tail()) nerr := normalizeFlags(a.Flags, set) context := NewContext(a, set, ctx) if nerr != nil { - fmt.Fprintln(a.Writer, nerr) - fmt.Fprintln(a.Writer) + _, _ = fmt.Fprintln(a.Writer, nerr) + _, _ = fmt.Fprintln(a.Writer) if len(a.Commands) > 0 { - ShowSubcommandHelp(context) + _ = ShowSubcommandHelp(context) } else { - ShowCommandHelp(ctx, context.Args().First()) + _ = ShowCommandHelp(ctx, context.Args().First()) } return nerr } @@ -332,11 +403,11 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { if err != nil { if a.OnUsageError != nil { err = a.OnUsageError(context, err, true) - HandleExitCoder(err) + a.handleExitCoder(context, err) return err } - fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) - ShowSubcommandHelp(context) + _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) + _ = ShowSubcommandHelp(context) return err } @@ -350,11 +421,17 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } } + cerr := checkRequiredFlags(a.Flags, context) + if cerr != nil { + _ = ShowSubcommandHelp(context) + return cerr + } + if a.After != nil { defer func() { afterErr := a.After(context) if afterErr != nil { - HandleExitCoder(err) + a.handleExitCoder(context, err) if err != nil { err = newMultiError(err, afterErr) } else { @@ -367,7 +444,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { if a.Before != nil { beforeErr := a.Before(context) if beforeErr != nil { - HandleExitCoder(beforeErr) + a.handleExitCoder(context, beforeErr) err = beforeErr return err } @@ -385,7 +462,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { // Run default Action err = a.Action(context) - HandleExitCoder(err) + a.handleExitCoder(context, err) return err } @@ -419,7 +496,7 @@ func (a *App) VisibleCategories() []CommandCategory { // VisibleCommands returns a slice of the Commands with Hidden=false func (a *App) VisibleCommands() []*Command { - ret := []*Command{} + var ret []*Command for _, command := range a.Commands { if !command.Hidden { ret = append(ret, command) @@ -444,7 +521,6 @@ func (a *App) hasFlag(flag Flag) bool { } func (a *App) errWriter() io.Writer { - // When the app ErrWriter is nil use the package level one. if a.ErrWriter == nil { return ErrWriter @@ -465,6 +541,14 @@ func (a *App) appendCommand(c *Command) { } } +func (a *App) handleExitCoder(context *Context, err error) { + if a.ExitErrHandler != nil { + a.ExitErrHandler(context, err) + } else { + HandleExitCoder(err) + } +} + // Author represents someone who has contributed to a cli project. type Author struct { Name string // The Authors name @@ -488,3 +572,20 @@ func DefaultCommand(name string) ActionFunc { return ctx.App.Command(name).Run(ctx) } } + +// HandleAction attempts to figure out which Action signature was used. If +// it's an ActionFunc or a func with the legacy signature for Action, the func +// is run! +func HandleAction(action interface{}, context *Context) (err error) { + switch a := action.(type) { + case ActionFunc: + return a(context) + case func(*Context) error: + return a(context) + case func(*Context): // deprecated function signature + a(context) + return nil + } + + return errInvalidActionType +} diff --git a/app_test.go b/app_test.go index 84631fd..e4bbbcb 100644 --- a/app_test.go +++ b/app_test.go @@ -86,7 +86,7 @@ func ExampleApp_Run_subcommand() { }, } - app.Run(os.Args) + _ = app.Run(os.Args) // Output: // Hello, Jeremy } @@ -119,7 +119,7 @@ func ExampleApp_Run_appHelp() { }, }, } - app.Run(os.Args) + _ = app.Run(os.Args) // Output: // NAME: // greet - A new cli application @@ -138,8 +138,8 @@ func ExampleApp_Run_appHelp() { // Oliver Allen // // COMMANDS: - // describeit, d use it to see a description - // help, h Shows a list of commands or help for one command + // describeit, d use it to see a description + // help, h Shows a list of commands or help for one command // // GLOBAL OPTIONS: // --name value a name to say (default: "bob") @@ -169,7 +169,7 @@ func ExampleApp_Run_commandHelp() { }, }, } - app.Run(os.Args) + _ = app.Run(os.Args) // Output: // NAME: // greet describeit - use it to see a description @@ -184,7 +184,7 @@ func ExampleApp_Run_commandHelp() { func ExampleApp_Run_noAction() { app := App{} app.Name = "greet" - app.Run([]string{"greet"}) + _ = app.Run([]string{"greet"}) // Output: // NAME: // greet - A new cli application @@ -196,7 +196,7 @@ func ExampleApp_Run_noAction() { // 0.0.0 // // COMMANDS: - // help, h Shows a list of commands or help for one command + // help, h Shows a list of commands or help for one command // // GLOBAL OPTIONS: // --help, -h show help (default: false) @@ -215,7 +215,7 @@ func ExampleApp_Run_subcommandNoAction() { }, }, } - app.Run([]string{"greet", "describeit"}) + _ = app.Run([]string{"greet", "describeit"}) // Output: // NAME: // greet describeit - use it to see a description @@ -231,13 +231,103 @@ func ExampleApp_Run_subcommandNoAction() { } -func ExampleApp_Run_shellComplete() { +func ExampleApp_Run_bashComplete_withShortFlag() { + os.Args = []string{"greet", "-", "--generate-bash-completion"} + + app := NewApp() + app.Name = "greet" + app.EnableBashCompletion = true + app.Flags = []Flag{ + &IntFlag{ + Name: "other", + Aliases: []string{"o"}, + }, + &StringFlag{ + Name: "xyz", + Aliases: []string{"x"}, + }, + } + + _ = app.Run(os.Args) + // Output: + // --other + // -o + // --xyz + // -x + // --help + // -h + // --version + // -v +} + +func ExampleApp_Run_bashComplete_withLongFlag() { + os.Args = []string{"greet", "--s", "--generate-bash-completion"} + + app := NewApp() + app.Name = "greet" + app.EnableBashCompletion = true + app.Flags = []Flag{ + &IntFlag{ + Name: "other", + Aliases: []string{"o"}, + }, + &StringFlag{ + Name: "xyz", + Aliases: []string{"x"}, + }, + &StringFlag{ + Name: "some-flag,s", + }, + &StringFlag{ + Name: "similar-flag", + }, + } + + _ = app.Run(os.Args) + // Output: + // --some-flag + // --similar-flag +} +func ExampleApp_Run_bashComplete_withMultipleLongFlag() { + os.Args = []string{"greet", "--st", "--generate-bash-completion"} + + app := NewApp() + app.Name = "greet" + app.EnableBashCompletion = true + app.Flags = []Flag{ + &IntFlag{ + Name: "int-flag", + Aliases: []string{"i"}, + }, + &StringFlag{ + Name: "string", + Aliases: []string{"s"}, + }, + &StringFlag{ + Name: "string-flag-2", + }, + &StringFlag{ + Name: "similar-flag", + }, + &StringFlag{ + Name: "some-flag", + }, + } + + _ = app.Run(os.Args) + // Output: + // --string + // --string-flag-2 +} + +func ExampleApp_Run_bashComplete() { + // set args for examples sake // set args for examples sake - os.Args = []string{"greet", fmt.Sprintf("--%s", genCompName())} + os.Args = []string{"greet", "--generate-bash-completion"} app := &App{ Name: "greet", - EnableShellCompletion: true, + EnableBashCompletion: true, Commands: []*Command{ { Name: "describeit", @@ -260,7 +350,7 @@ func ExampleApp_Run_shellComplete() { }, } - app.Run(os.Args) + _ = app.Run(os.Args) // Output: // describeit // d @@ -269,6 +359,44 @@ func ExampleApp_Run_shellComplete() { // h } +func ExampleApp_Run_zshComplete() { + // set args for examples sake + os.Args = []string{"greet", "--generate-bash-completion"} + _ = os.Setenv("_CLI_ZSH_AUTOCOMPLETE_HACK", "1") + + app := NewApp() + app.Name = "greet" + app.EnableBashCompletion = true + app.Commands = []*Command{ + { + Name: "describeit", + Aliases: []string{"d"}, + Usage: "use it to see a description", + Description: "This is how we describe describeit the function", + Action: func(c *Context) error { + fmt.Printf("i like to describe things") + return nil + }, + }, { + Name: "next", + Usage: "next example", + Description: "more stuff to see when generating bash completion", + Action: func(c *Context) error { + fmt.Printf("the next example") + return nil + }, + }, + } + + _ = app.Run(os.Args) + // Output: + // describeit:use it to see a description + // d:use it to see a description + // next:next example + // help:Shows a list of commands or help for one command + // h:Shows a list of commands or help for one command +} + func TestApp_Run(t *testing.T) { s := "" @@ -317,6 +445,63 @@ func TestApp_Setup_defaultsWriter(t *testing.T) { expect(t, app.Writer, os.Stdout) } + +func TestApp_CommandWithArgBeforeFlags(t *testing.T) { + var parsedOption, firstArg string + + app := NewApp() + command := &Command{ + Name: "cmd", + Flags: []Flag{ + &StringFlag{Name: "option", Value: "", Usage: "some option"}, + }, + Action: func(c *Context) error { + parsedOption = c.String("option") + firstArg = c.Args().First() + return nil + }, + } + app.Commands = []*Command{command} + + _ = app.Run([]string{"", "cmd", "my-arg", "--option", "my-option"}) + + expect(t, parsedOption, "my-option") + expect(t, firstArg, "my-arg") +} + +func TestApp_CommandWithArgBeforeBoolFlags(t *testing.T) { + var parsedOption, parsedSecondOption, firstArg string + var parsedBool, parsedSecondBool bool + + app := NewApp() + command := &Command{ + Name: "cmd", + Flags: []Flag{ + &StringFlag{Name: "option", Value: "", Usage: "some option"}, + &StringFlag{Name: "secondOption", Value: "", Usage: "another option"}, + &BoolFlag{Name: "boolflag", Usage: "some bool"}, + &BoolFlag{Name: "b", Usage: "another bool"}, + }, + Action: func(c *Context) error { + parsedOption = c.String("option") + parsedSecondOption = c.String("secondOption") + parsedBool = c.Bool("boolflag") + parsedSecondBool = c.Bool("b") + firstArg = c.Args().First() + return nil + }, + } + app.Commands = []*Command{command} + + _ = app.Run([]string{"", "cmd", "my-arg", "--boolflag", "--option", "my-option", "-b", "--secondOption", "fancy-option"}) + + expect(t, parsedOption, "my-option") + expect(t, parsedSecondOption, "fancy-option") + expect(t, parsedBool, true) + expect(t, parsedSecondBool, true) + expect(t, firstArg, "my-arg") +} + func TestApp_RunAsSubcommandParseFlags(t *testing.T) { var context *Context @@ -339,7 +524,7 @@ func TestApp_RunAsSubcommandParseFlags(t *testing.T) { }, }, } - a.Run([]string{"", "foo", "--lang", "spanish", "abcd"}) + _ = a.Run([]string{"", "foo", "--lang", "spanish", "abcd"}) expect(t, context.Args().Get(0), "abcd") expect(t, context.String("lang"), "spanish") @@ -355,7 +540,7 @@ func TestApp_RunAsSubCommandIncorrectUsage(t *testing.T) { } set := flag.NewFlagSet("", flag.ContinueOnError) - set.Parse([]string{"", "---foo"}) + _ = set.Parse([]string{"", "---foo"}) c := &Context{flagSet: set} err := a.RunAsSubcommand(c) @@ -383,7 +568,7 @@ func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { }, } - app.Run([]string{"", "cmd", "--option", "my-option", "my-arg", "--", "--notARealFlag"}) + _ = app.Run([]string{"", "cmd", "my-arg", "--option", "my-option", "--", "--notARealFlag"}) expect(t, parsedOption, "my-option") expect(t, args.Get(0), "my-arg") @@ -406,7 +591,7 @@ func TestApp_CommandWithDash(t *testing.T) { }, } - app.Run([]string{"", "cmd", "my-arg", "-"}) + _ = app.Run([]string{"", "cmd", "my-arg", "-"}) expect(t, args.Get(0), "my-arg") expect(t, args.Get(1), "-") @@ -427,7 +612,7 @@ func TestApp_CommandWithNoFlagBeforeTerminator(t *testing.T) { }, } - app.Run([]string{"", "cmd", "my-arg", "--", "notAFlagAtAll"}) + _ = app.Run([]string{"", "cmd", "my-arg", "--", "notAFlagAtAll"}) expect(t, args.Get(0), "my-arg") expect(t, args.Get(1), "--") @@ -485,6 +670,93 @@ func TestApp_VisibleCommands(t *testing.T) { } } +func TestApp_UseShortOptionHandling(t *testing.T) { + var one, two bool + var name string + expected := "expectedName" + + app := NewApp() + app.UseShortOptionHandling = true + app.Flags = []Flag{ + &BoolFlag{Name: "one", Aliases: []string{"o"}}, + &BoolFlag{Name: "two", Aliases: []string{"t"}}, + &StringFlag{Name: "name", Aliases: []string{"n"}}, + } + app.Action = func(c *Context) error { + one = c.Bool("one") + two = c.Bool("two") + name = c.String("name") + return nil + } + + _ = app.Run([]string{"", "-on", expected}) + expect(t, one, true) + expect(t, two, false) + expect(t, name, expected) +} + +func TestApp_UseShortOptionHandlingCommand(t *testing.T) { + var one, two bool + var name string + expected := "expectedName" + + app := NewApp() + app.UseShortOptionHandling = true + command := &Command{ + Name: "cmd", + Flags: []Flag{ + &BoolFlag{Name: "one", Aliases: []string{"o"}}, + &BoolFlag{Name: "two", Aliases: []string{"t"}}, + &StringFlag{Name: "name", Aliases: []string{"n"}}, + }, + Action: func(c *Context) error { + one = c.Bool("one") + two = c.Bool("two") + name = c.String("name") + return nil + }, + } + app.Commands = []*Command{command} + + _ = app.Run([]string{"", "cmd", "-on", expected}) + expect(t, one, true) + expect(t, two, false) + expect(t, name, expected) +} + +func TestApp_UseShortOptionHandlingSubCommand(t *testing.T) { + var one, two bool + var name string + expected := "expectedName" + + app := NewApp() + app.UseShortOptionHandling = true + command := &Command{ + Name: "cmd", + } + subCommand := &Command{ + Name: "sub", + Flags: []Flag{ + &BoolFlag{Name: "one", Aliases: []string{"o"}}, + &BoolFlag{Name: "two", Aliases: []string{"t"}}, + &StringFlag{Name: "name", Aliases: []string{"n"}}, + }, + Action: func(c *Context) error { + one = c.Bool("one") + two = c.Bool("two") + name = c.String("name") + return nil + }, + } + app.Commands = []*Command{command, subCommand} + + err := app.Run([]string{"", "cmd", "sub", "-on", expected}) + expect(t, err, nil) + expect(t, one, true) + expect(t, two, false) + expect(t, name, expected) +} + func TestApp_Float64Flag(t *testing.T) { var meters float64 @@ -498,7 +770,7 @@ func TestApp_Float64Flag(t *testing.T) { }, } - app.Run([]string{"", "--height", "1.93"}) + _ = app.Run([]string{"", "--height", "1.93"}) expect(t, meters, 1.93) } @@ -528,7 +800,7 @@ func TestApp_ParseSliceFlags(t *testing.T) { var _ = parsedOption var _ = firstArg - app.Run([]string{"", "cmd", "-p", "22", "-p", "80", "-ip", "8.8.8.8", "-ip", "8.8.4.4", "my-arg"}) + _ = app.Run([]string{"", "cmd", "my-arg", "-p", "22", "-p", "80", "-ip", "8.8.8.8", "-ip", "8.8.4.4"}) IntsEquals := func(a, b []int) bool { if len(a) != len(b) { @@ -586,7 +858,7 @@ func TestApp_ParseSliceFlagsWithMissingValue(t *testing.T) { }, } - app.Run([]string{"", "cmd", "-a", "2", "-str", "A", "my-arg"}) + _ = app.Run([]string{"", "cmd", "my-arg", "-a", "2", "-str", "A"}) var expectedIntSlice = []int{2} var expectedStringSlice = []string{"A"} @@ -818,6 +1090,145 @@ func TestAppNoHelpFlag(t *testing.T) { } } +func TestRequiredFlagAppRunBehavior(t *testing.T) { + tdata := []struct { + testCase string + appFlags []Flag + appRunInput []string + appCommands []*Command + expectedAnError bool + }{ + // assertion: empty input, when a required flag is present, errors + { + testCase: "error_case_empty_input_with_required_flag_on_app", + appRunInput: []string{"myCLI"}, + appFlags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, + expectedAnError: true, + }, + { + testCase: "error_case_empty_input_with_required_flag_on_command", + appRunInput: []string{"myCLI", "myCommand"}, + appCommands: []*Command{{ + Name: "myCommand", + Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, + }}, + expectedAnError: true, + }, + { + testCase: "error_case_empty_input_with_required_flag_on_subcommand", + appRunInput: []string{"myCLI", "myCommand", "mySubCommand"}, + appCommands: []*Command{{ + Name: "myCommand", + Subcommands: []*Command{{ + Name: "mySubCommand", + Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, + }}, + }}, + expectedAnError: true, + }, + // assertion: inputing --help, when a required flag is present, does not error + { + testCase: "valid_case_help_input_with_required_flag_on_app", + appRunInput: []string{"myCLI", "--help"}, + appFlags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, + }, + { + testCase: "valid_case_help_input_with_required_flag_on_command", + appRunInput: []string{"myCLI", "myCommand", "--help"}, + appCommands: []*Command{{ + Name: "myCommand", + Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, + }}, + }, + { + testCase: "valid_case_help_input_with_required_flag_on_subcommand", + appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--help"}, + appCommands: []*Command{{ + Name: "myCommand", + Subcommands: []*Command{{ + Name: "mySubCommand", + Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, + }}, + }}, + }, + // assertion: giving optional input, when a required flag is present, errors + { + testCase: "error_case_optional_input_with_required_flag_on_app", + appRunInput: []string{"myCLI", "--optional", "cats"}, + appFlags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "optional"}}, + expectedAnError: true, + }, + { + testCase: "error_case_optional_input_with_required_flag_on_command", + appRunInput: []string{"myCLI", "myCommand", "--optional", "cats"}, + appCommands: []*Command{{ + Name: "myCommand", + Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "optional"}}, + }}, + expectedAnError: true, + }, + { + testCase: "error_case_optional_input_with_required_flag_on_subcommand", + appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--optional", "cats"}, + appCommands: []*Command{{ + Name: "myCommand", + Subcommands: []*Command{{ + Name: "mySubCommand", + Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}, &StringFlag{Name: "optional"}}, + }}, + }}, + expectedAnError: true, + }, + // assertion: when a required flag is present, inputting that required flag does not error + { + testCase: "valid_case_required_flag_input_on_app", + appRunInput: []string{"myCLI", "--requiredFlag", "cats"}, + appFlags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, + }, + { + testCase: "valid_case_required_flag_input_on_command", + appRunInput: []string{"myCLI", "myCommand", "--requiredFlag", "cats"}, + appCommands: []*Command{{ + Name: "myCommand", + Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, + }}, + }, + { + testCase: "valid_case_required_flag_input_on_subcommand", + appRunInput: []string{"myCLI", "myCommand", "mySubCommand", "--requiredFlag", "cats"}, + appCommands: []*Command{{ + Name: "myCommand", + Subcommands: []*Command{{ + Name: "mySubCommand", + Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, + }}, + }}, + }, + } + for _, test := range tdata { + t.Run(test.testCase, func(t *testing.T) { + // setup + app := NewApp() + app.Flags = test.appFlags + app.Commands = test.appCommands + + // logic under test + err := app.Run(test.appRunInput) + + // assertions + if test.expectedAnError && err == nil { + t.Errorf("expected an error, but there was none") + } + if _, ok := err.(requiredFlagsErr); test.expectedAnError && !ok { + t.Errorf("expected a requiredFlagsErr, but got: %s", err) + } + if !test.expectedAnError && err != nil { + t.Errorf("did not expected an error, but there was one: %s", err) + } + }) + } +} + func TestAppHelpPrinter(t *testing.T) { oldPrinter := HelpPrinter defer func() { @@ -830,7 +1241,7 @@ func TestAppHelpPrinter(t *testing.T) { } app := &App{} - app.Run([]string{"-h"}) + _ = app.Run([]string{"-h"}) if wasCalled == false { t.Errorf("Help printer expected to be called, but was not") @@ -876,7 +1287,7 @@ func TestApp_CommandNotFound(t *testing.T) { }, } - app.Run([]string{"command", "foo"}) + _ = app.Run([]string{"command", "foo"}) expect(t, counts.CommandNotFound, 1) expect(t, counts.SubCommand, 0) @@ -889,9 +1300,9 @@ func TestApp_OrderOfOperations(t *testing.T) { resetCounts := func() { counts = &opCounts{} } app := &App{ - EnableShellCompletion: true, - ShellComplete: func(c *Context) { - fmt.Fprintf(os.Stderr, "---> ShellComplete(%#v)\n", c) + EnableBashCompletion: true, + BashComplete: func(c *Context) { + _, _ = fmt.Fprintf(os.Stderr, "---> BashComplete(%#v)\n", c) counts.Total++ counts.ShellComplete = counts.Total }, @@ -956,7 +1367,7 @@ func TestApp_OrderOfOperations(t *testing.T) { resetCounts() - _ = app.Run([]string{"command", fmt.Sprintf("--%s", genCompName())}) + _ = app.Run([]string{"command", fmt.Sprintf("--%s", "--generate-bash-completion")}) expect(t, counts.ShellComplete, 1) expect(t, counts.Total, 1) @@ -1306,7 +1717,7 @@ func TestApp_Run_Categories(t *testing.T) { Writer: buf, } - app.Run([]string{"categories"}) + _ = app.Run([]string{"categories"}) expect := commandCategories([]*commandCategory{ { @@ -1569,6 +1980,18 @@ func (c *customBoolFlag) Names() []string { return []string{c.Nombre} } +func (c *customBoolFlag) TakesValue() bool { + return false +} + +func (c *customBoolFlag) GetValue() string { + return "value" +} + +func (c *customBoolFlag) GetUsage() string { + return "usage" +} + func (c *customBoolFlag) Apply(set *flag.FlagSet) { set.String(c.Nombre, c.Nombre, "") } @@ -1613,6 +2036,59 @@ func TestCustomHelpVersionFlags(t *testing.T) { } } +func TestHandleExitCoder_Default(t *testing.T) { + app := NewApp() + fs, err := flagSet(app.Name, app.Flags) + if err != nil { + t.Errorf("error creating FlagSet: %s", err) + } + + ctx := NewContext(app, fs, nil) + app.handleExitCoder(ctx, NewExitError("Default Behavior Error", 42)) + + output := fakeErrWriter.String() + if !strings.Contains(output, "Default") { + t.Fatalf("Expected Default Behavior from Error Handler but got: %s", output) + } +} + +func TestHandleExitCoder_Custom(t *testing.T) { + app := NewApp() + fs, err := flagSet(app.Name, app.Flags) + if err != nil { + t.Errorf("error creating FlagSet: %s", err) + } + + app.ExitErrHandler = func(_ *Context, _ error) { + _, _ = fmt.Fprintln(ErrWriter, "I'm a Custom error handler, I print what I want!") + } + + ctx := NewContext(app, fs, nil) + app.handleExitCoder(ctx, NewExitError("Default Behavior Error", 42)) + + output := fakeErrWriter.String() + if !strings.Contains(output, "Custom") { + t.Fatalf("Expected Custom Behavior from Error Handler but got: %s", output) + } +} + +func TestHandleAction_WithUnknownPanic(t *testing.T) { + defer func() { refute(t, recover(), nil) }() + + var fn ActionFunc + + app := NewApp() + app.Action = func(ctx *Context) error { + _ = fn(ctx) + return nil + } + fs, err := flagSet(app.Name, app.Flags) + if err != nil { + t.Errorf("error creating FlagSet: %s", err) + } + _ = HandleAction(app.Action, NewContext(app, fs, nil)) +} + func TestShellCompletionForIncompleteFlags(t *testing.T) { app := &App{ Flags: []Flag{ @@ -1620,30 +2096,30 @@ func TestShellCompletionForIncompleteFlags(t *testing.T) { Name: "test-completion", }, }, - EnableShellCompletion: true, - ShellComplete: func(ctx *Context) { + EnableBashCompletion: true, + BashComplete: func(ctx *Context) { for _, command := range ctx.App.Commands { if command.Hidden { continue } for _, name := range command.Names() { - fmt.Fprintln(ctx.App.Writer, name) + _, _ = fmt.Fprintln(ctx.App.Writer, name) } } for _, flag := range ctx.App.Flags { for _, name := range flag.Names() { - if name == genCompName() { + if name == BashCompletionFlag.Names()[0] { continue } switch name = strings.TrimSpace(name); len(name) { case 0: case 1: - fmt.Fprintln(ctx.App.Writer, "-"+name) + _, _ = fmt.Fprintln(ctx.App.Writer, "-"+name) default: - fmt.Fprintln(ctx.App.Writer, "--"+name) + _, _ = fmt.Fprintln(ctx.App.Writer, "--"+name) } } } @@ -1652,8 +2128,62 @@ func TestShellCompletionForIncompleteFlags(t *testing.T) { return fmt.Errorf("should not get here") }, } - err := app.Run([]string{"", "--test-completion", "--" + genCompName()}) + err := app.Run([]string{"", "--test-completion", "--" + "generate-bash-completion"}) if err != nil { t.Errorf("app should not return an error: %s", err) } } + +func TestWhenExitSubCommandWithCodeThenAppQuitUnexpectedly(t *testing.T) { + testCode := 104 + + app := NewApp() + app.Commands = []*Command{ + { + Name: "cmd", + Subcommands: []*Command{ + { + Name: "subcmd", + Action: func(c *Context) error { + return NewExitError("exit error", testCode) + }, + }, + }, + }, + } + + // set user function as ExitErrHandler + var exitCodeFromExitErrHandler int + app.ExitErrHandler = func(c *Context, err error) { + if exitErr, ok := err.(ExitCoder); ok { + t.Log(exitErr) + exitCodeFromExitErrHandler = exitErr.ExitCode() + } + } + + // keep and restore original OsExiter + origExiter := OsExiter + defer func() { + OsExiter = origExiter + }() + + // set user function as OsExiter + var exitCodeFromOsExiter int + OsExiter = func(exitCode int) { + exitCodeFromOsExiter = exitCode + } + + _ = app.Run([]string{ + "myapp", + "cmd", + "subcmd", + }) + + if exitCodeFromOsExiter != 0 { + t.Errorf("exitCodeFromOsExiter should not change, but its value is %v", exitCodeFromOsExiter) + } + + if exitCodeFromExitErrHandler != testCode { + t.Errorf("exitCodeFromOsExiter valeu should be %v, but its value is %v", testCode, exitCodeFromExitErrHandler) + } +} diff --git a/appveyor.yml b/appveyor.yml index 886f0e9..a8c05a1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,26 +11,17 @@ cache: environment: GOPATH: C:\gopath - GOVERSION: 1.8.x - PYTHON: C:\Python36-x64 - PYTHON_VERSION: 3.6.x - PYTHON_ARCH: 64 + GOVERSION: 1.11.x install: -- set PATH=%GOPATH%\bin;C:\go\bin;%PATH% -- go version -- go env -- go get github.com/urfave/gfmrun/... -- rmdir c:\gopath\src\gopkg.in\urfave\cli.v2 /s /q -- rmdir c:\gopath\pkg /s /q -- git clone . c:\gopath\src\gopkg.in\urfave\cli.v2 -- go get -v -t ./... -- if not exist node_modules\.bin\markdown-toc npm install markdown-toc + - set PATH=%GOPATH%\bin;C:\go\bin;%PATH% + - go version + - go env + - go get github.com/urfave/gfmrun/... + - go get -v -t ./... build_script: -- python runtests vet -- python runtests test -- python runtests gfmrun -- python cli-v1-to-v2 --selftest -- python runtests migrations -- python runtests toc + - go run build.go vet + - go run build.go test + - go run build.go gfmrun + diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index 37d9c14..f0f6241 100755 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -3,14 +3,19 @@ : ${PROG:=$(basename ${BASH_SOURCE})} _cli_bash_autocomplete() { + if [[ "${COMP_WORDS[0]}" != "source" ]]; then local cur opts base COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" - opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + if [[ "$cur" == "-"* ]]; then + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) + else + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + fi COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 + fi } -complete -F _cli_bash_autocomplete $PROG - +complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG unset PROG diff --git a/autocomplete/zsh_autocomplete b/autocomplete/zsh_autocomplete index 5430a18..8b747ae 100644 --- a/autocomplete/zsh_autocomplete +++ b/autocomplete/zsh_autocomplete @@ -1,5 +1,11 @@ -autoload -U compinit && compinit -autoload -U bashcompinit && bashcompinit +_cli_zsh_autocomplete() { -script_dir=$(dirname $0) -source ${script_dir}/bash_autocomplete + local -a opts + opts=("${(@f)$(_CLI_ZSH_AUTOCOMPLETE_HACK=1 ${words[@]:0:#words[@]-1} --generate-bash-completion)}") + + _describe 'values' opts + + return +} + +compdef _cli_zsh_autocomplete $PROG diff --git a/build.go b/build.go new file mode 100644 index 0000000..a828ae4 --- /dev/null +++ b/build.go @@ -0,0 +1,164 @@ +//+build ignore + +package main + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "strings" + + "github.com/urfave/cli" +) + +var packages = []string{"cli", "altsrc"} + +func main() { + app := cli.NewApp() + + app.Name = "builder" + app.Usage = "Generates a new urfave/cli build!" + + app.Commands = cli.Commands{ + cli.Command{ + Name: "vet", + Action: VetActionFunc, + }, + cli.Command{ + Name: "test", + Action: TestActionFunc, + }, + cli.Command{ + Name: "gfmrun", + Action: GfmrunActionFunc, + }, + cli.Command{ + Name: "toc", + Action: TocActionFunc, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} + +func runCmd(arg string, args ...string) error { + cmd := exec.Command(arg, args...) + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +func VetActionFunc(_ *cli.Context) error { + return runCmd("go", "vet") +} + +func TestActionFunc(c *cli.Context) error { + for _, pkg := range packages { + var packageName string + + if pkg == "cli" { + packageName = "github.com/urfave/cli" + } else { + packageName = fmt.Sprintf("github.com/urfave/cli/%s", pkg) + } + + coverProfile := fmt.Sprintf("--coverprofile=%s.coverprofile", pkg) + + err := runCmd("go", "test", "-v", coverProfile, packageName) + if err != nil { + return err + } + } + + return testCleanup() +} + +func testCleanup() error { + var out bytes.Buffer + + for _, pkg := range packages { + file, err := os.Open(fmt.Sprintf("%s.coverprofile", pkg)) + if err != nil { + return err + } + + b, err := ioutil.ReadAll(file) + if err != nil { + return err + } + + out.Write(b) + err = file.Close() + if err != nil { + return err + } + + err = os.Remove(fmt.Sprintf("%s.coverprofile", pkg)) + if err != nil { + return err + } + } + + outFile, err := os.Create("coverage.txt") + if err != nil { + return err + } + + _, err = out.WriteTo(outFile) + if err != nil { + return err + } + + err = outFile.Close() + if err != nil { + return err + } + + return nil +} + +func GfmrunActionFunc(_ *cli.Context) error { + file, err := os.Open("README.md") + if err != nil { + return err + } + + var counter int + scanner := bufio.NewScanner(file) + for scanner.Scan() { + if strings.Contains(scanner.Text(), "package main") { + counter++ + } + } + + err = scanner.Err() + if err != nil { + return err + } + + return runCmd("gfmrun", "-c", fmt.Sprint(counter), "-s", "README.md") +} + +func TocActionFunc(_ *cli.Context) error { + err := runCmd("node_modules/.bin/markdown-toc", "-i", "README.md") + if err != nil { + return err + } + + err = runCmd("git", "diff", "--exit-code") + if err != nil { + return err + } + + return nil +} diff --git a/category.go b/category.go index 3b405c0..64d84ec 100644 --- a/category.go +++ b/category.go @@ -15,7 +15,7 @@ func newCommandCategories() CommandCategories { } func (c *commandCategories) Less(i, j int) bool { - return (*c)[i].Name() < (*c)[j].Name() + return lexicographicLess((*c)[i].Name(), (*c)[j].Name() ) } func (c *commandCategories) Len() int { @@ -35,7 +35,7 @@ func (c *commandCategories) AddCommand(category string, command *Command) { } newVal := commandCategories(append(*c, &commandCategory{name: category, commands: []*Command{command}})) - (*c) = newVal + *c = newVal } func (c *commandCategories) Categories() []CommandCategory { @@ -75,7 +75,7 @@ func (c *commandCategory) VisibleCommands() []*Command { c.commands = []*Command{} } - ret := []*Command{} + var ret []*Command for _, command := range c.commands { if !command.Hidden { ret = append(ret, command) diff --git a/cli.go b/cli.go index 81fc7ab..62a5bc2 100644 --- a/cli.go +++ b/cli.go @@ -20,4 +20,4 @@ // } package cli -//go:generate python ./generate-flag-types cli -i flag-types.json -o flag_generated.go +//go:generate go run flag-gen/main.go flag-gen/assets_vfsdata.go diff --git a/command.go b/command.go index 05093b8..181091e 100644 --- a/command.go +++ b/command.go @@ -1,6 +1,7 @@ package cli import ( + "flag" "fmt" "io/ioutil" "sort" @@ -23,8 +24,8 @@ type Command struct { ArgsUsage string // The category the command is part of Category string - // The function to call when checking for shell command completions - ShellComplete ShellCompleteFunc + // The function to call when checking for bash command completions + BashComplete BashCompleteFunc // An action to execute before any sub-subcommands are run, but after the context is ready // If a non-nil error is returned, no sub-subcommands are run Before BeforeFunc @@ -45,6 +46,10 @@ type Command struct { HideHelp bool // Boolean to hide this command from help or completion Hidden bool + // Boolean to enable short-option handling so user can combine several + // single-character bool arguments into one + // i.e. foobar -o -v -> foobar -ov + UseShortOptionHandling bool // Full name of command for help, defaults to full command name, including parent commands. HelpName string @@ -63,7 +68,7 @@ func (c CommandsByName) Len() int { } func (c CommandsByName) Less(i, j int) bool { - return c[i].Name < c[j].Name + return lexicographicLess(c[i].Name, c[j].Name) } func (c CommandsByName) Swap(i, j int) { @@ -90,10 +95,14 @@ func (c *Command) Run(ctx *Context) (err error) { c.appendFlag(HelpFlag) } - if ctx.App.EnableShellCompletion { - c.appendFlag(GenerateCompletionFlag) + if ctx.App.UseShortOptionHandling { + c.UseShortOptionHandling = true } + //if ctx.App.EnableShellCompletion { + // c.appendFlag(GenerateCompletionFlag) + //} + set, err := flagSet(c.Name, c.Flags) if err != nil { return err @@ -108,9 +117,9 @@ func (c *Command) Run(ctx *Context) (err error) { nerr := normalizeFlags(c.Flags, set) if nerr != nil { - fmt.Fprintln(ctx.App.Writer, nerr) - fmt.Fprintln(ctx.App.Writer) - ShowCommandHelp(ctx, c.Name) + _, _ = fmt.Fprintln(ctx.App.Writer, nerr) + _, _ = fmt.Fprintln(ctx.App.Writer) + _ = ShowCommandHelp(ctx, c.Name) return nerr } @@ -126,9 +135,9 @@ func (c *Command) Run(ctx *Context) (err error) { HandleExitCoder(err) return err } - fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) - fmt.Fprintln(context.App.Writer) - ShowCommandHelp(context, c.Name) + _, _ = fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) + _, _ = fmt.Fprintln(context.App.Writer) + _ = ShowCommandHelp(context, c.Name) return err } @@ -136,6 +145,12 @@ func (c *Command) Run(ctx *Context) (err error) { return nil } + cerr := checkRequiredFlags(c.Flags, context) + if cerr != nil { + _ = ShowCommandHelp(context, c.Name) + return cerr + } + if c.After != nil { defer func() { afterErr := c.After(context) @@ -172,6 +187,14 @@ func (c *Command) Run(ctx *Context) (err error) { return err } +func (c *Command) newFlagSet() (*flag.FlagSet, error) { + return flagSet(c.Name, c.Flags) +} + +func (c *Command) useShortOptionHandling() bool { + return c.UseShortOptionHandling +} + // Names returns the names including short names and aliases. func (c *Command) Names() []string { return append([]string{c.Name}, c.Aliases...) @@ -217,6 +240,7 @@ func (c *Command) startApp(ctx *Context) error { app.Compiled = ctx.App.Compiled app.Writer = ctx.App.Writer app.ErrWriter = ctx.App.ErrWriter + app.UseShortOptionHandling = ctx.App.UseShortOptionHandling app.Categories = newCommandCategories() for _, command := range c.Subcommands { @@ -226,9 +250,9 @@ func (c *Command) startApp(ctx *Context) error { sort.Sort(app.Categories.(*commandCategories)) // bash completion - app.EnableShellCompletion = ctx.App.EnableShellCompletion - if c.ShellComplete != nil { - app.ShellComplete = c.ShellComplete + app.EnableBashCompletion = ctx.App.EnableBashCompletion + if c.BashComplete != nil { + app.BashComplete = c.BashComplete } // set the actions diff --git a/command_test.go b/command_test.go index 7fb3980..f2ccf9a 100644 --- a/command_test.go +++ b/command_test.go @@ -11,32 +11,40 @@ import ( func TestCommandFlagParsing(t *testing.T) { cases := []struct { - testArgs []string - skipFlagParsing bool - expectedErr error + testArgs []string + skipFlagParsing bool + skipArgReorder bool + expectedErr error + UseShortOptionHandling bool }{ // Test normal "not ignoring flags" flow - {[]string{"test-cmd", "-break", "blah", "blah"}, false, errors.New("flag provided but not defined: -break")}, + {[]string{"test-cmd", "blah", "blah", "-break"}, false, false, errors.New("flag provided but not defined: -break"), false}, - {[]string{"test-cmd", "blah", "blah"}, true, nil}, // Test SkipFlagParsing without any args that look like flags - {[]string{"test-cmd", "blah", "-break"}, true, nil}, // Test SkipFlagParsing with random flag arg - {[]string{"test-cmd", "blah", "-help"}, true, nil}, // Test SkipFlagParsing with "special" help flag arg + // Test no arg reorder + {[]string{"test-cmd", "blah", "blah", "-break"}, false, true, nil, false}, + {[]string{"test-cmd", "blah", "blah", "-break", "ls", "-l"}, false, true, nil, true}, + + {[]string{"test-cmd", "blah", "blah"}, true, false, nil, false}, // Test SkipFlagParsing without any args that look like flags + {[]string{"test-cmd", "blah", "-break"}, true, false, nil, false}, // Test SkipFlagParsing with random flag arg + {[]string{"test-cmd", "blah", "-help"}, true, false, nil, false}, // Test SkipFlagParsing with "special" help flag arg + {[]string{"test-cmd", "blah"}, false, false, nil, true}, // Test UseShortOptionHandling } for _, c := range cases { app := &App{Writer: ioutil.Discard} set := flag.NewFlagSet("test", 0) - set.Parse(c.testArgs) + _ = set.Parse(c.testArgs) context := NewContext(app, set, nil) command := Command{ - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(_ *Context) error { return nil }, - SkipFlagParsing: c.skipFlagParsing, + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(_ *Context) error { return nil }, + SkipFlagParsing: c.skipFlagParsing, + UseShortOptionHandling: c.UseShortOptionHandling, } err := command.Run(context) @@ -46,6 +54,51 @@ func TestCommandFlagParsing(t *testing.T) { } } +func TestParseAndRunShortOpts(t *testing.T) { + cases := []struct { + testArgs []string + expectedErr error + expectedArgs []string + }{ + {[]string{"foo", "test", "-a"}, nil, []string{}}, + {[]string{"foo", "test", "-c", "arg1", "arg2"}, nil, []string{"arg1", "arg2"}}, + {[]string{"foo", "test", "-f"}, nil, []string{}}, + {[]string{"foo", "test", "-ac", "--fgh"}, nil, []string{}}, + {[]string{"foo", "test", "-af"}, nil, []string{}}, + {[]string{"foo", "test", "-cf"}, nil, []string{}}, + {[]string{"foo", "test", "-acf"}, nil, []string{}}, + {[]string{"foo", "test", "-invalid"}, errors.New("flag provided but not defined: -invalid"), []string{}}, + {[]string{"foo", "test", "-acf", "arg1", "-invalid"}, nil, []string{"arg1", "-invalid"}}, + } + + var args Args + cmd := Command{ + Name: "test", + Usage: "this is for testing", + Description: "testing", + Action: func(c *Context) error { + args = c.Args() + return nil + }, + UseShortOptionHandling: true, + Flags: []Flag{ + &BoolFlag{Name: "abc", Aliases: []string{"a"}}, + &BoolFlag{Name: "cde", Aliases: []string{"c"}}, + &BoolFlag{Name: "fgh", Aliases: []string{"f"}}, + }, + } + + for _, c := range cases { + app := NewApp() + app.Commands = []Command{cmd} + + err := app.Run(c.testArgs) + + expect(t, err, c.expectedErr) + expect(t, args, c.expectedArgs) + } +} + func TestCommand_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { app := &App{ Commands: []*Command{ @@ -238,3 +291,77 @@ func TestCommand_Run_SubcommandsCanUseErrWriter(t *testing.T) { t.Fatal(err) } } + +func TestCommandFlagReordering(t *testing.T) { + cases := []struct { + testArgs []string + expectedValue string + expectedArgs []string + expectedErr error + }{ + {[]string{"some-exec", "some-command", "some-arg", "--flag", "foo"}, "foo", []string{"some-arg"}, nil}, + {[]string{"some-exec", "some-command", "some-arg", "--flag=foo"}, "foo", []string{"some-arg"}, nil}, + {[]string{"some-exec", "some-command", "--flag=foo", "some-arg"}, "foo", []string{"some-arg"}, nil}, + } + + for _, c := range cases { + value := "" + var args Args + app := &App{ + Commands: []*Command{ + { + Name: "some-command", + Flags: []Flag{ + &StringFlag{Name: "flag"}, + }, + Action: func(c *Context) error { + fmt.Printf("%+v\n", c.String("flag")) + value = c.String("flag") + args = c.Args() + return nil + }, + }, + }, + } + + err := app.Run(c.testArgs) + expect(t, err, c.expectedErr) + expect(t, value, c.expectedValue) + expect(t, args, c.expectedArgs) + } +} + +func TestCommandSkipFlagParsing(t *testing.T) { + cases := []struct { + testArgs []string + expectedArgs []string + expectedErr error + }{ + {[]string{"some-exec", "some-command", "some-arg", "--flag", "foo"}, []string{"some-arg", "--flag", "foo"}, nil}, + {[]string{"some-exec", "some-command", "some-arg", "--flag=foo"}, []string{"some-arg", "--flag=foo"}, nil}, + } + + for _, c := range cases { + var args Args + app := &App{ + Commands: []*Command{ + { + SkipFlagParsing: true, + Name: "some-command", + Flags: []Flag{ + &StringFlag{Name: "flag"}, + }, + Action: func(c *Context) error { + fmt.Printf("%+v\n", c.String("flag")) + args = c.Args() + return nil + }, + }, + }, + } + + err := app.Run(c.testArgs) + expect(t, err, c.expectedErr) + expect(t, args, c.expectedArgs) + } +} diff --git a/context.go b/context.go index f1a01b4..ba477f6 100644 --- a/context.go +++ b/context.go @@ -4,6 +4,7 @@ import ( "context" "errors" "flag" + "fmt" "os" "os/signal" "reflect" @@ -20,7 +21,7 @@ type Context struct { App *App Command *Command shellComplete bool - + setFlags map[string]bool flagSet *flag.FlagSet parentContext *Context } @@ -65,41 +66,56 @@ func (c *Context) IsSet(name string) bool { isSet = true } }) - if isSet { - return true - } - } - // XXX hack to support IsSet for flags with EnvVar - // - // There isn't an easy way to do this with the current implementation since - // whether a flag was set via an environment variable is very difficult to - // determine here. Instead, we intend to introduce a backwards incompatible - // change in version 2 to add `IsSet` to the Flag interface to push the - // responsibility closer to where the information required to determine - // whether a flag is set by non-standard means such as environment - // variables is avaliable. - // - // See https://github.com/urfave/cli/issues/294 for additional discussion - f := lookupFlag(name, c) - if f == nil { - return false - } + // XXX hack to support IsSet for flags with EnvVar + // + // There isn't an easy way to do this with the current implementation since + // whether a flag was set via an environment variable is very difficult to + // determine here. Instead, we intend to introduce a backwards incompatible + // change in version 2 to add `IsSet` to the Flag interface to push the + // responsibility closer to where the information required to determine + // whether a flag is set by non-standard means such as environment + // variables is available. + // + // See https://github.com/urfave/cli/issues/294 for additional discussion + flags := c.Command.Flags + if c.Command.Name == "" { // cannot == Command{} since it contains slice types + if c.App != nil { + flags = c.App.Flags + } + } + for _, f := range flags { + for _, name := range f.Names() { + if isSet, ok := c.setFlags[name]; isSet || !ok { + continue + } - val := reflect.ValueOf(f) - if val.Kind() == reflect.Ptr { - val = val.Elem() - } + val := reflect.ValueOf(f) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } - envVarValues := val.FieldByName("EnvVars") - if !envVarValues.IsValid() { - return false - } + filePathValue := val.FieldByName("FilePath") + if filePathValue.IsValid() { + eachName(filePathValue.String(), func(filePath string) { + if _, err := os.Stat(filePath); err == nil { + c.setFlags[name] = true + return + } + }) + } - for _, envVar := range envVarValues.Interface().([]string) { - envVar = strings.TrimSpace(envVar) - if envVal := os.Getenv(envVar); envVal != "" { - return true + envVarValues := val.FieldByName("EnvVars") + if envVarValues.IsValid() { + for _, envVar := range envVarValues.Interface().([]string) { + envVar = strings.TrimSpace(envVar) + if _, ok := syscall.Getenv(envVar); ok { + c.setFlags[name] = true + continue + } + } + } + } } } @@ -108,7 +124,7 @@ func (c *Context) IsSet(name string) bool { // LocalFlagNames returns a slice of flag names used in this context. func (c *Context) LocalFlagNames() []string { - names := []string{} + var names []string c.flagSet.Visit(makeFlagNameVisitor(&names)) return names } @@ -116,17 +132,42 @@ func (c *Context) LocalFlagNames() []string { // FlagNames returns a slice of flag names used by the this context and all of // its parent contexts. func (c *Context) FlagNames() []string { - names := []string{} + var names []string for _, ctx := range c.Lineage() { ctx.flagSet.Visit(makeFlagNameVisitor(&names)) + } return names } +// FlagNames returns a slice of flag names used in this context. +//func (c *Context) FlagNames() (names []string) { +// for _, f := range c.Command.Flags { +// name := strings.Split(f.GetName(), ",")[0] +// if name == "help" { +// continue +// } +// names = append(names, name) +// } +// return +//} + +// GlobalFlagNames returns a slice of global flag names used by the app. +//func (c *Context) GlobalFlagNames() (names []string) { +// for _, f := range c.App.Flags { +// name := strings.Split(f.GetName(), ",")[0] +// if name == "help" || name == "version" { +// continue +// } +// names = append(names, name) +// } +// return names +//} + // Lineage returns *this* context and all of its ancestor contexts in order from // child to parent func (c *Context) Lineage() []*Context { - lineage := []*Context{} + var lineage []*Context for cur := c; cur != nil; cur = cur.parentContext { lineage = append(lineage, cur) @@ -191,10 +232,10 @@ func lookupFlagSet(name string, ctx *Context) *flag.FlagSet { func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { switch ff.Value.(type) { - case Serializeder: - set.Set(name, ff.Value.(Serializeder).Serialized()) + case Serializer: + _ = set.Set(name, ff.Value.(Serializer).Serialize()) default: - set.Set(name, ff.Value.String()) + _ = set.Set(name, ff.Value.String()) } } @@ -244,7 +285,58 @@ func makeFlagNameVisitor(names *[]string) func(*flag.Flag) { } if name != "" { - (*names) = append(*names, name) + *names = append(*names, name) } } } + +type requiredFlagsErr interface { + error + getMissingFlags() []string +} + +type errRequiredFlags struct { + missingFlags []string +} + +func (e *errRequiredFlags) Error() string { + numberOfMissingFlags := len(e.missingFlags) + if numberOfMissingFlags == 1 { + return fmt.Sprintf("Required flag %q not set", e.missingFlags[0]) + } + joinedMissingFlags := strings.Join(e.missingFlags, ", ") + return fmt.Sprintf("Required flags %q not set", joinedMissingFlags) +} + +func (e *errRequiredFlags) getMissingFlags() []string { + return e.missingFlags +} + +func checkRequiredFlags(flags []Flag, context *Context) requiredFlagsErr { + var missingFlags []string + for _, f := range flags { + if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { + var flagPresent bool + var flagName string + for _, key := range f.Names() { + if len(key) > 1 { + flagName = key + } + + if context.IsSet(strings.TrimSpace(key)) { + flagPresent = true + } + } + + if !flagPresent && flagName != "" { + missingFlags = append(missingFlags, flagName) + } + } + } + + if len(missingFlags) != 0 { + return &errRequiredFlags{missingFlags: missingFlags} + } + + return nil +} diff --git a/context_test.go b/context_test.go index 7333ae0..c15528b 100644 --- a/context_test.go +++ b/context_test.go @@ -3,7 +3,12 @@ package cli import ( "context" "flag" + "sort" + + "os" + "strings" + "testing" "time" ) @@ -91,9 +96,11 @@ func TestContext_Float64(t *testing.T) { func TestContext_Duration(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Duration("myflag", 12*time.Second, "doc") + parentSet := flag.NewFlagSet("test", 0) parentSet.Duration("top-flag", 13*time.Second, "doc") parentCtx := NewContext(nil, parentSet, nil) + c := NewContext(nil, set, parentCtx) expect(t, c.Duration("myflag"), 12*time.Second) expect(t, c.Duration("top-flag"), 13*time.Second) @@ -136,7 +143,7 @@ func TestContext_Args(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("myflag", false, "doc") c := NewContext(nil, set, nil) - set.Parse([]string{"--myflag", "bat", "baz"}) + _ = set.Parse([]string{"--myflag", "bat", "baz"}) expect(t, c.Args().Len(), 2) expect(t, c.Bool("myflag"), true) } @@ -145,7 +152,7 @@ func TestContext_NArg(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("myflag", false, "doc") c := NewContext(nil, set, nil) - set.Parse([]string{"--myflag", "bat", "baz"}) + _ = set.Parse([]string{"--myflag", "bat", "baz"}) expect(t, c.NArg(), 2) } @@ -159,8 +166,8 @@ func TestContext_IsSet(t *testing.T) { parentCtx := NewContext(nil, parentSet, nil) ctx := NewContext(nil, set, parentCtx) - set.Parse([]string{"--one-flag", "--two-flag", "--three-flag", "frob"}) - parentSet.Parse([]string{"--top-flag"}) + _ = set.Parse([]string{"--one-flag", "--two-flag", "--three-flag", "frob"}) + _ = parentSet.Parse([]string{"--top-flag"}) expect(t, ctx.IsSet("one-flag"), true) expect(t, ctx.IsSet("two-flag"), true) @@ -169,6 +176,121 @@ func TestContext_IsSet(t *testing.T) { expect(t, ctx.IsSet("bogus"), false) } +// XXX Corresponds to hack in context.IsSet for flags with EnvVar field +// Should be moved to `flag_test` in v2 +func TestContext_IsSet_fromEnv(t *testing.T) { + var ( + timeoutIsSet, tIsSet bool + noEnvVarIsSet, nIsSet bool + passwordIsSet, pIsSet bool + unparsableIsSet, uIsSet bool + ) + + os.Clearenv() + _ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5") + _ = os.Setenv("APP_PASSWORD", "") + a := App{ + Flags: []Flag{ + &Float64Flag{Name: "timeout", Aliases: []string{"t"}, EnvVars: []string{"APP_TIMEOUT_SECONDS"}}, + &StringFlag{Name: "password", Aliases: []string{"p"}, EnvVars: []string{"APP_PASSWORD"}}, + &Float64Flag{Name: "unparsable", Aliases: []string{"u"}, EnvVars: []string{"APP_UNPARSABLE"}}, + &Float64Flag{Name: "no-env-var", Aliases: []string{"n"}}, + }, + Action: func(ctx *Context) error { + timeoutIsSet = ctx.IsSet("timeout") + tIsSet = ctx.IsSet("t") + passwordIsSet = ctx.IsSet("password") + pIsSet = ctx.IsSet("p") + unparsableIsSet = ctx.IsSet("unparsable") + uIsSet = ctx.IsSet("u") + noEnvVarIsSet = ctx.IsSet("no-env-var") + nIsSet = ctx.IsSet("n") + return nil + }, + } + _ = a.Run([]string{"run"}) + expect(t, timeoutIsSet, true) + expect(t, tIsSet, true) + expect(t, passwordIsSet, true) + expect(t, pIsSet, true) + expect(t, noEnvVarIsSet, false) + expect(t, nIsSet, false) + + _ = os.Setenv("APP_UNPARSABLE", "foobar") + _ = a.Run([]string{"run"}) + expect(t, unparsableIsSet, false) + expect(t, uIsSet, false) +} + +// XXX Corresponds to hack in context.IsSet for flags with EnvVar field +// TODO: Should be moved to `flag_test` in v2 +//func TestContext_GlobalIsSet_fromEnv(t *testing.T) { +// var ( +// timeoutIsSet, tIsSet bool +// noEnvVarIsSet, nIsSet bool +// passwordIsSet, pIsSet bool +// passwordValue string +// unparsableIsSet, uIsSet bool +// overrideIsSet, oIsSet bool +// overrideValue string +// ) +// +// os.Clearenv() +// _ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5") +// _ = os.Setenv("APP_PASSWORD", "badpass") +// _ = os.Setenv("APP_OVERRIDE", "overridden") +// a := App{ +// Flags: []Flag{ +// Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, +// StringFlag{Name: "password, p", EnvVar: "APP_PASSWORD"}, +// Float64Flag{Name: "no-env-var, n"}, +// Float64Flag{Name: "unparsable, u", EnvVar: "APP_UNPARSABLE"}, +// StringFlag{Name: "overrides-default, o", Value: "default", EnvVar: "APP_OVERRIDE"}, +// }, +// Commands: []Command{ +// { +// Name: "hello", +// Action: func(ctx *Context) error { +// timeoutIsSet = ctx.GlobalIsSet("timeout") +// tIsSet = ctx.GlobalIsSet("t") +// passwordIsSet = ctx.GlobalIsSet("password") +// pIsSet = ctx.GlobalIsSet("p") +// passwordValue = ctx.GlobalString("password") +// unparsableIsSet = ctx.GlobalIsSet("unparsable") +// uIsSet = ctx.GlobalIsSet("u") +// noEnvVarIsSet = ctx.GlobalIsSet("no-env-var") +// nIsSet = ctx.GlobalIsSet("n") +// overrideIsSet = ctx.GlobalIsSet("overrides-default") +// oIsSet = ctx.GlobalIsSet("o") +// overrideValue = ctx.GlobalString("overrides-default") +// return nil +// }, +// }, +// }, +// } +// if err := a.Run([]string{"run", "hello"}); err != nil { +// t.Logf("error running Run(): %+v", err) +// } +// expect(t, timeoutIsSet, true) +// expect(t, tIsSet, true) +// expect(t, passwordIsSet, true) +// expect(t, pIsSet, true) +// expect(t, passwordValue, "badpass") +// expect(t, unparsableIsSet, false) +// expect(t, noEnvVarIsSet, false) +// expect(t, nIsSet, false) +// expect(t, overrideIsSet, true) +// expect(t, oIsSet, true) +// expect(t, overrideValue, "overridden") +// +// _ = os.Setenv("APP_UNPARSABLE", "foobar") +// if err := a.Run([]string{"run"}); err != nil { +// t.Logf("error running Run(): %+v", err) +// } +// expect(t, unparsableIsSet, false) +// expect(t, uIsSet, false) +//} + func TestContext_NumFlags(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("myflag", false, "doc") @@ -177,8 +299,8 @@ func TestContext_NumFlags(t *testing.T) { globalSet.Bool("myflagGlobal", true, "doc") globalCtx := NewContext(nil, globalSet, nil) c := NewContext(nil, set, globalCtx) - set.Parse([]string{"--myflag", "--otherflag=foo"}) - globalSet.Parse([]string{"--myflagGlobal"}) + _ = set.Parse([]string{"--myflag", "--otherflag=foo"}) + _ = globalSet.Parse([]string{"--myflagGlobal"}) expect(t, c.NumFlags(), 2) } @@ -188,7 +310,7 @@ func TestContext_Set(t *testing.T) { c := NewContext(nil, set, nil) expect(t, c.IsSet("int"), false) - c.Set("int", "1") + _ = c.Set("int", "1") expect(t, c.Int("int"), 1) expect(t, c.IsSet("int"), true) } @@ -293,3 +415,153 @@ func TestContextPropagation(t *testing.T) { t.Fatal("expected context to not be nil even if the parent's context is nil") } } + +func TestCheckRequiredFlags(t *testing.T) { + tdata := []struct { + testCase string + parseInput []string + envVarInput [2]string + flags []Flag + expectedAnError bool + expectedErrorContents []string + }{ + { + testCase: "empty", + }, + { + testCase: "optional", + flags: []Flag{ + &StringFlag{Name: "optionalFlag"}, + }, + }, + { + testCase: "required", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + }, + expectedAnError: true, + expectedErrorContents: []string{"requiredFlag"}, + }, + { + testCase: "required_and_present", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + }, + parseInput: []string{"--requiredFlag", "myinput"}, + }, + { + testCase: "required_and_present_via_env_var", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true, EnvVar: "REQUIRED_FLAG"}, + }, + envVarInput: [2]string{"REQUIRED_FLAG", "true"}, + }, + { + testCase: "required_and_optional", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "optionalFlag"}, + }, + expectedAnError: true, + }, + { + testCase: "required_and_optional_and_optional_present", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "optionalFlag"}, + }, + parseInput: []string{"--optionalFlag", "myinput"}, + expectedAnError: true, + }, + { + testCase: "required_and_optional_and_optional_present_via_env_var", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "optionalFlag", EnvVar: "OPTIONAL_FLAG"}, + }, + envVarInput: [2]string{"OPTIONAL_FLAG", "true"}, + expectedAnError: true, + }, + { + testCase: "required_and_optional_and_required_present", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "optionalFlag"}, + }, + parseInput: []string{"--requiredFlag", "myinput"}, + }, + { + testCase: "two_required", + flags: []Flag{ + &StringFlag{Name: "requiredFlagOne", Required: true}, + &StringFlag{Name: "requiredFlagTwo", Required: true}, + }, + expectedAnError: true, + expectedErrorContents: []string{"requiredFlagOne", "requiredFlagTwo"}, + }, + { + testCase: "two_required_and_one_present", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "requiredFlagTwo", Required: true}, + }, + parseInput: []string{"--requiredFlag", "myinput"}, + expectedAnError: true, + }, + { + testCase: "two_required_and_both_present", + flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + &StringFlag{Name: "requiredFlagTwo", Required: true}, + }, + parseInput: []string{"--requiredFlag", "myinput", "--requiredFlagTwo", "myinput"}, + }, + { + testCase: "required_flag_with_short_name", + flags: []Flag{ + &StringSliceFlag{Name: "names, N", Required: true}, + }, + parseInput: []string{"-N", "asd", "-N", "qwe"}, + }, + { + testCase: "required_flag_with_multiple_short_names", + flags: []Flag{ + &StringSliceFlag{Name: "names, N, n", Required: true}, + }, + parseInput: []string{"-n", "asd", "-n", "qwe"}, + }, + } + for _, test := range tdata { + t.Run(test.testCase, func(t *testing.T) { + // setup + set := flag.NewFlagSet("test", 0) + for _, flags := range test.flags { + flags.Apply(set) + } + _ = set.Parse(test.parseInput) + if test.envVarInput[0] != "" { + os.Clearenv() + _ = os.Setenv(test.envVarInput[0], test.envVarInput[1]) + } + ctx := &Context{} + context := NewContext(ctx.App, set, ctx) + context.Command.Flags = test.flags + + // logic under test + err := checkRequiredFlags(test.flags, context) + + // assertions + if test.expectedAnError && err == nil { + t.Errorf("expected an error, but there was none") + } + if !test.expectedAnError && err != nil { + t.Errorf("did not expected an error, but there was one: %s", err) + } + for _, errString := range test.expectedErrorContents { + if !strings.Contains(err.Error(), errString) { + t.Errorf("expected error %q to contain %q, but it didn't!", err.Error(), errString) + } + } + }) + } +} diff --git a/docs.go b/docs.go new file mode 100644 index 0000000..5b94566 --- /dev/null +++ b/docs.go @@ -0,0 +1,148 @@ +package cli + +import ( + "bytes" + "fmt" + "io" + "sort" + "strings" + "text/template" + + "github.com/cpuguy83/go-md2man/v2/md2man" +) + +// ToMarkdown creates a markdown string for the `*App` +// The function errors if either parsing or writing of the string fails. +func (a *App) ToMarkdown() (string, error) { + var w bytes.Buffer + if err := a.writeDocTemplate(&w); err != nil { + return "", err + } + return w.String(), nil +} + +// ToMan creates a man page string for the `*App` +// The function errors if either parsing or writing of the string fails. +func (a *App) ToMan() (string, error) { + var w bytes.Buffer + if err := a.writeDocTemplate(&w); err != nil { + return "", err + } + man := md2man.Render(w.Bytes()) + return string(man), nil +} + +type cliTemplate struct { + App *App + Commands []string + GlobalArgs []string + SynopsisArgs []string +} + +func (a *App) writeDocTemplate(w io.Writer) error { + const name = "cli" + t, err := template.New(name).Parse(MarkdownDocTemplate) + if err != nil { + return err + } + return t.ExecuteTemplate(w, name, &cliTemplate{ + App: a, + Commands: prepareCommands(a.Commands, 0), + GlobalArgs: prepareArgsWithValues(a.Flags), + SynopsisArgs: prepareArgsSynopsis(a.Flags), + }) +} + +func prepareCommands(commands []Command, level int) []string { + coms := []string{} + for i := range commands { + command := &commands[i] + if command.Hidden { + continue + } + usage := "" + if command.Usage != "" { + usage = command.Usage + } + + prepared := fmt.Sprintf("%s %s\n\n%s\n", + strings.Repeat("#", level+2), + strings.Join(command.Names(), ", "), + usage, + ) + + flags := prepareArgsWithValues(command.Flags) + if len(flags) > 0 { + prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n")) + } + + coms = append(coms, prepared) + + // recursevly iterate subcommands + if len(command.Subcommands) > 0 { + coms = append( + coms, + prepareCommands(command.Subcommands, level+1)..., + ) + } + } + + return coms +} + +func prepareArgsWithValues(flags []Flag) []string { + return prepareFlags(flags, ", ", "**", "**", `""`, true) +} + +func prepareArgsSynopsis(flags []Flag) []string { + return prepareFlags(flags, "|", "[", "]", "[value]", false) +} + +func prepareFlags( + flags []Flag, + sep, opener, closer, value string, + addDetails bool, +) []string { + args := []string{} + for _, f := range flags { + flag, ok := f.(DocGenerationFlag) + if !ok { + continue + } + modifiedArg := opener + for _, s := range strings.Split(flag.GetName(), ",") { + trimmed := strings.TrimSpace(s) + if len(modifiedArg) > len(opener) { + modifiedArg += sep + } + if len(trimmed) > 1 { + modifiedArg += fmt.Sprintf("--%s", trimmed) + } else { + modifiedArg += fmt.Sprintf("-%s", trimmed) + } + } + modifiedArg += closer + if flag.TakesValue() { + modifiedArg += fmt.Sprintf("=%s", value) + } + + if addDetails { + modifiedArg += flagDetails(flag) + } + + args = append(args, modifiedArg+"\n") + + } + sort.Strings(args) + return args +} + +// flagDetails returns a string containing the flags metadata +func flagDetails(flag DocGenerationFlag) string { + description := flag.GetUsage() + value := flag.GetValue() + if value != "" { + description += " (default: " + value + ")" + } + return ": " + description +} diff --git a/docs_test.go b/docs_test.go new file mode 100644 index 0000000..f18d12d --- /dev/null +++ b/docs_test.go @@ -0,0 +1,122 @@ +package cli + +import ( + "io/ioutil" + "testing" +) + +func testApp() *App { + app := NewApp() + app.Name = "greet" + app.Flags = []Flag{ + StringFlag{ + Name: "socket, s", + Usage: "some 'usage' text", + Value: "value", + TakesFile: true, + }, + StringFlag{Name: "flag, fl, f"}, + BoolFlag{ + Name: "another-flag, b", + Usage: "another usage text", + }, + } + app.Commands = []Command{{ + Aliases: []string{"c"}, + Flags: []Flag{ + StringFlag{ + Name: "flag, fl, f", + TakesFile: true, + }, + BoolFlag{ + Name: "another-flag, b", + Usage: "another usage text", + }, + }, + Name: "config", + Usage: "another usage test", + Subcommands: []Command{{ + Aliases: []string{"s", "ss"}, + Flags: []Flag{ + StringFlag{Name: "sub-flag, sub-fl, s"}, + BoolFlag{ + Name: "sub-command-flag, s", + Usage: "some usage text", + }, + }, + Name: "sub-config", + Usage: "another usage test", + }}, + }, { + Aliases: []string{"i", "in"}, + Name: "info", + Usage: "retrieve generic information", + }, { + Name: "some-command", + }, { + Name: "hidden-command", + Hidden: true, + }} + app.UsageText = "app [first_arg] [second_arg]" + app.Usage = "Some app" + app.Author = "Harrison" + app.Email = "harrison@lolwut.com" + app.Authors = []Author{{Name: "Oliver Allen", Email: "oliver@toyshop.com"}} + return app +} + +func expectFileContent(t *testing.T, file, expected string) { + data, err := ioutil.ReadFile(file) + expect(t, err, nil) + expect(t, string(data), expected) +} + +func TestToMarkdownFull(t *testing.T) { + // Given + app := testApp() + + // When + res, err := app.ToMarkdown() + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-doc-full.md", res) +} + +func TestToMarkdownNoFlags(t *testing.T) { + // Given + app := testApp() + app.Flags = nil + + // When + res, err := app.ToMarkdown() + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-doc-no-flags.md", res) +} + +func TestToMarkdownNoCommands(t *testing.T) { + // Given + app := testApp() + app.Commands = nil + + // When + res, err := app.ToMarkdown() + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-doc-no-commands.md", res) +} + +func TestToMan(t *testing.T) { + // Given + app := testApp() + + // When + res, err := app.ToMan() + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-doc-full.man", res) +} diff --git a/errors.go b/errors.go index 6259699..2f12338 100644 --- a/errors.go +++ b/errors.go @@ -59,25 +59,33 @@ type ExitCoder interface { ExitCode() int } -type exitError struct { +type ExitError struct { exitCode int message interface{} } +// NewExitError makes a new *ExitError +func NewExitError(message interface{}, exitCode int) *ExitError { + return &ExitError{ + exitCode: exitCode, + message: message, + } +} + // Exit wraps a message and exit code into an ExitCoder suitable for handling by // HandleExitCoder func Exit(message interface{}, exitCode int) ExitCoder { - return &exitError{ + return &ExitError{ exitCode: exitCode, message: message, } } -func (ee *exitError) Error() string { +func (ee *ExitError) Error() string { return fmt.Sprintf("%v", ee.message) } -func (ee *exitError) ExitCode() int { +func (ee *ExitError) ExitCode() int { return ee.exitCode } diff --git a/fish.go b/fish.go new file mode 100644 index 0000000..cf183af --- /dev/null +++ b/fish.go @@ -0,0 +1,194 @@ +package cli + +import ( + "bytes" + "fmt" + "io" + "strings" + "text/template" +) + +// ToFishCompletion creates a fish completion string for the `*App` +// The function errors if either parsing or writing of the string fails. +func (a *App) ToFishCompletion() (string, error) { + var w bytes.Buffer + if err := a.writeFishCompletionTemplate(&w); err != nil { + return "", err + } + return w.String(), nil +} + +type fishCompletionTemplate struct { + App *App + Completions []string + AllCommands []string +} + +func (a *App) writeFishCompletionTemplate(w io.Writer) error { + const name = "cli" + t, err := template.New(name).Parse(FishCompletionTemplate) + if err != nil { + return err + } + allCommands := []string{} + + // Add global flags + completions := a.prepareFishFlags(a.VisibleFlags(), allCommands) + + // Add help flag + if !a.HideHelp { + completions = append( + completions, + a.prepareFishFlags([]Flag{HelpFlag}, allCommands)..., + ) + } + + // Add version flag + if !a.HideVersion { + completions = append( + completions, + a.prepareFishFlags([]Flag{VersionFlag}, allCommands)..., + ) + } + + // Add commands and their flags + completions = append( + completions, + a.prepareFishCommands(a.VisibleCommands(), &allCommands, []string{})..., + ) + + return t.ExecuteTemplate(w, name, &fishCompletionTemplate{ + App: a, + Completions: completions, + AllCommands: allCommands, + }) +} + +func (a *App) prepareFishCommands(commands []Command, allCommands *[]string, previousCommands []string) []string { + completions := []string{} + for i := range commands { + command := &commands[i] + + if command.Hidden { + continue + } + + var completion strings.Builder + completion.WriteString(fmt.Sprintf( + "complete -r -c %s -n '%s' -a '%s'", + a.Name, + a.fishSubcommandHelper(previousCommands), + strings.Join(command.Names(), " "), + )) + + if command.Usage != "" { + completion.WriteString(fmt.Sprintf(" -d '%s'", + escapeSingleQuotes(command.Usage))) + } + + if !command.HideHelp { + completions = append( + completions, + a.prepareFishFlags([]Flag{HelpFlag}, command.Names())..., + ) + } + + *allCommands = append(*allCommands, command.Names()...) + completions = append(completions, completion.String()) + completions = append( + completions, + a.prepareFishFlags(command.Flags, command.Names())..., + ) + + // recursevly iterate subcommands + if len(command.Subcommands) > 0 { + completions = append( + completions, + a.prepareFishCommands( + command.Subcommands, allCommands, command.Names(), + )..., + ) + } + } + + return completions +} + +func (a *App) prepareFishFlags(flags []Flag, previousCommands []string) []string { + completions := []string{} + for _, f := range flags { + flag, ok := f.(DocGenerationFlag) + if !ok { + continue + } + + completion := &strings.Builder{} + completion.WriteString(fmt.Sprintf( + "complete -c %s -n '%s'", + a.Name, + a.fishSubcommandHelper(previousCommands), + )) + + fishAddFileFlag(f, completion) + + for idx, opt := range strings.Split(flag.GetName(), ",") { + if idx == 0 { + completion.WriteString(fmt.Sprintf( + " -l %s", strings.TrimSpace(opt), + )) + } else { + completion.WriteString(fmt.Sprintf( + " -s %s", strings.TrimSpace(opt), + )) + + } + } + + if flag.TakesValue() { + completion.WriteString(" -r") + } + + if flag.GetUsage() != "" { + completion.WriteString(fmt.Sprintf(" -d '%s'", + escapeSingleQuotes(flag.GetUsage()))) + } + + completions = append(completions, completion.String()) + } + + return completions +} + +func fishAddFileFlag(flag Flag, completion *strings.Builder) { + switch f := flag.(type) { + case GenericFlag: + if f.TakesFile { + return + } + case StringFlag: + if f.TakesFile { + return + } + case StringSliceFlag: + if f.TakesFile { + return + } + } + completion.WriteString(" -f") +} + +func (a *App) fishSubcommandHelper(allCommands []string) string { + fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", a.Name) + if len(allCommands) > 0 { + fishHelper = fmt.Sprintf( + "__fish_seen_subcommand_from %s", + strings.Join(allCommands, " "), + ) + } + return fishHelper + +} + +func escapeSingleQuotes(input string) string { + return strings.Replace(input, `'`, `\'`, -1) +} diff --git a/fish_test.go b/fish_test.go new file mode 100644 index 0000000..a4c1871 --- /dev/null +++ b/fish_test.go @@ -0,0 +1,17 @@ +package cli + +import ( + "testing" +) + +func TestFishCompletion(t *testing.T) { + // Given + app := testApp() + + // When + res, err := app.ToFishCompletion() + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-fish-full.fish", res) +} diff --git a/flag-types.json b/flag-types.json deleted file mode 100644 index bd5ec3f..0000000 --- a/flag-types.json +++ /dev/null @@ -1,98 +0,0 @@ -[ - { - "name": "Bool", - "type": "bool", - "context_default": "false", - "parser": "strconv.ParseBool(f.Value.String())" - }, - { - "name": "Duration", - "type": "time.Duration", - "doctail": " (see https://golang.org/pkg/time/#ParseDuration)", - "context_default": "0", - "parser": "time.ParseDuration(f.Value.String())" - }, - { - "name": "Float64", - "type": "float64", - "context_default": "0", - "parser": "strconv.ParseFloat(f.Value.String(), 64)" - }, - { - "name": "Generic", - "type": "Generic", - "dest": false, - "context_default": "nil", - "context_type": "interface{}" - }, - { - "name": "Int64", - "type": "int64", - "context_default": "0", - "parser": "strconv.ParseInt(f.Value.String(), 0, 64)" - }, - { - "name": "Int", - "type": "int", - "context_default": "0", - "parser": "strconv.ParseInt(f.Value.String(), 0, 64)", - "parser_cast": "int(parsed)" - }, - { - "name": "IntSlice", - "type": "*IntSlice", - "dest": false, - "context_default": "nil", - "context_type": "[]int", - "parser": "(f.Value.(*IntSlice)).Value(), error(nil)" - }, - { - "name": "Int64Slice", - "type": "*Int64Slice", - "dest": false, - "context_default": "nil", - "context_type": "[]int64", - "parser": "(f.Value.(*Int64Slice)).Value(), error(nil)" - }, - { - "name": "Float64Slice", - "type": "*Float64Slice", - "dest": false, - "context_default": "nil", - "context_type": "[]float64", - "parser": "(f.Value.(*Float64Slice)).Value(), error(nil)" - }, - { - "name": "String", - "type": "string", - "context_default": "\"\"", - "parser": "f.Value.String(), error(nil)" - }, - { - "name": "Path", - "type": "string", - "context_default": "\"\"", - "parser": "f.Value.String(), error(nil)" - }, - { - "name": "StringSlice", - "type": "*StringSlice", - "dest": false, - "context_default": "nil", - "context_type": "[]string", - "parser": "(f.Value.(*StringSlice)).Value(), error(nil)" - }, - { - "name": "Uint64", - "type": "uint64", - "context_default": "0", - "parser": "strconv.ParseUint(f.Value.String(), 0, 64)" - }, - { - "name": "Uint", - "type": "uint", - "context_default": "0", - "parser": "strconv.ParseUint(f.Value.String(), 0, 64)", - "parser_cast": "uint(parsed)" - } -] diff --git a/flag.go b/flag.go index 364362c..6a32574 100644 --- a/flag.go +++ b/flag.go @@ -1,9 +1,9 @@ package cli import ( - "encoding/json" "flag" "fmt" + "io/ioutil" "reflect" "regexp" "runtime" @@ -22,50 +22,66 @@ var ( ) // GenerateCompletionFlag enables completion for all commands and subcommands -var GenerateCompletionFlag Flag = &BoolFlag{ - Name: "generate-completion", +//var GenerateCompletionFlag Flag = &BoolFlag{ +// Name: "generate-completion", +// Hidden: true, +//} +// +//func genCompName() string { +// names := GenerateCompletionFlag.Names() +// if len(names) == 0 { +// return "generate-completion" +// } +// return names[0] +//} +// +//// InitCompletionFlag generates completion code +//var InitCompletionFlag = &StringFlag{ +// Name: "init-completion", +// Usage: "generate completion code. Value must be 'bash' or 'zsh'", +//} + +// BashCompletionFlag enables bash-completion for all commands and subcommands +var BashCompletionFlag Flag = &BoolFlag{ + Name: "generate-bash-completion", Hidden: true, } -func genCompName() string { - names := GenerateCompletionFlag.Names() - if len(names) == 0 { - return "generate-completion" - } - return names[0] -} - -// InitCompletionFlag generates completion code -var InitCompletionFlag = &StringFlag{ - Name: "init-completion", - Usage: "generate completion code. Value must be 'bash' or 'zsh'", -} - // VersionFlag prints the version for the application var VersionFlag Flag = &BoolFlag{ - Name: "version", - Aliases: []string{"v"}, - Usage: "print the version", + Name: "version, v", + Usage: "print the version", } // HelpFlag prints the help for all commands and subcommands. // Set to nil to disable the flag. The subcommand // will still be added unless HideHelp is set to true. var HelpFlag Flag = &BoolFlag{ - Name: "help", - Aliases: []string{"h"}, - Usage: "show help", + Name: "help, h", + Usage: "show help", } // FlagStringer converts a flag definition to a string. This is used by help // to display a flag. var FlagStringer FlagStringFunc = stringifyFlag -// Serializeder is used to circumvent the limitations of flag.FlagSet.Set -type Serializeder interface { - Serialized() string +// Serializer is used to circumvent the limitations of flag.FlagSet.Set +type Serializer interface { + Serialize() string } +// FlagNamePrefixer converts a full flag name and its placeholder into the help +// message flag prefix. This is used by the default FlagStringer. +var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames + +// FlagEnvHinter annotates flag help message with the environment variable +// details. This is used by the default FlagStringer. +var FlagEnvHinter FlagEnvHintFunc = withEnvHint + +// FlagFileHinter annotates flag help message with the environment variable +// details. This is used by the default FlagStringer. +var FlagFileHinter FlagFileHintFunc = withFileHint + // FlagsByName is a slice of Flag. type FlagsByName []Flag @@ -74,12 +90,16 @@ func (f FlagsByName) Len() int { } func (f FlagsByName) Less(i, j int) bool { + //<<<<<<< HEAD if len(f[j].Names()) == 0 { return false } else if len(f[i].Names()) == 0 { return true } return f[i].Names()[0] < f[j].Names()[0] + //======= + // return lexicographicLess(f[i].GetName(), f[j].GetName()) + //>>>>>>> master } func (f FlagsByName) Swap(i, j int) { @@ -92,711 +112,59 @@ func (f FlagsByName) Swap(i, j int) { type Flag interface { fmt.Stringer // Apply Flag settings to the given flag set - Apply(*flag.FlagSet) + Apply(*flag.FlagSet) error Names() []string } -// errorableFlag is an interface that allows us to return errors during apply -// it allows flags defined in this library to return errors in a fashion backwards compatible -// TODO remove in v2 and modify the existing Flag interface to return errors -type errorableFlag interface { +// RequiredFlag is an interface that allows us to mark flags as required +// it allows flags required flags to be backwards compatible with the Flag interface +type RequiredFlag interface { Flag - ApplyWithError(*flag.FlagSet) error -} - -func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { - set := flag.NewFlagSet(name, flag.ContinueOnError) - - for _, f := range flags { - //TODO remove in v2 when errorableFlag is removed - if ef, ok := f.(errorableFlag); ok { - if err := ef.ApplyWithError(set); err != nil { - return nil, err - } - } else { - f.Apply(set) - } - } - return set, nil -} - -// Generic is a generic parseable type identified by a specific flag -type Generic interface { - Set(value string) error - String() string -} - -// Apply takes the flagset and calls Set on the generic flag with the value -// provided by the user for parsing by the flag -// Ignores parsing errors -func (f *GenericFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError takes the flagset and calls Set on the generic flag with the -// value provided by the user for parsing by the flag -func (f *GenericFlag) ApplyWithError(set *flag.FlagSet) error { - val := f.Value - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - val.Set(envVal) - break - } - } - } - - for _, name := range f.Names() { - set.Var(val, name, f.Usage) - } - return nil -} - -// StringSlice wraps a []string to satisfy flag.Value -type StringSlice struct { - slice []string - hasBeenSet bool -} - -// NewStringSlice creates a *StringSlice with default values -func NewStringSlice(defaults ...string) *StringSlice { - return &StringSlice{slice: append([]string{}, defaults...)} -} - -// Set appends the string value to the list of values -func (f *StringSlice) Set(value string) error { - if !f.hasBeenSet { - f.slice = []string{} - f.hasBeenSet = true - } - - if strings.HasPrefix(value, slPfx) { - // Deserializing assumes overwrite - _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &f.slice) - f.hasBeenSet = true - return nil - } - - f.slice = append(f.slice, value) - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *StringSlice) String() string { - return fmt.Sprintf("%s", f.slice) -} - -// Serialized allows StringSlice to fulfill Serializeder -func (f *StringSlice) Serialized() string { - jsonBytes, _ := json.Marshal(f.slice) - return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) -} - -// Value returns the slice of strings set by this flag -func (f *StringSlice) Value() []string { - return f.slice -} - -// Get returns the slice of strings set by this flag -func (f *StringSlice) Get() interface{} { - return *f -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *StringSliceFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f *StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - newVal := NewStringSlice() - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - if err := newVal.Set(s); err != nil { - return fmt.Errorf("could not parse %q as string value for flag %s: %s", envVal, f.Name, err) - } - } - f.Value = newVal - break - } - } - } - - if f.Value == nil { - f.Value = NewStringSlice() - } - - for _, name := range f.Names() { - set.Var(f.Value, name, f.Usage) - } - return nil -} - -// IntSlice wraps an []int to satisfy flag.Value -type IntSlice struct { - slice []int - hasBeenSet bool -} - -// NewIntSlice makes an *IntSlice with default values -func NewIntSlice(defaults ...int) *IntSlice { - return &IntSlice{slice: append([]int{}, defaults...)} -} - -// NewInt64Slice makes an *Int64Slice with default values -func NewInt64Slice(defaults ...int64) *Int64Slice { - return &Int64Slice{slice: append([]int64{}, defaults...)} -} - -// SetInt directly adds an integer to the list of values -func (i *IntSlice) SetInt(value int) { - if !i.hasBeenSet { - i.slice = []int{} - i.hasBeenSet = true - } - - i.slice = append(i.slice, value) -} - -// Set parses the value into an integer and appends it to the list of values -func (i *IntSlice) Set(value string) error { - if !i.hasBeenSet { - i.slice = []int{} - i.hasBeenSet = true - } - - if strings.HasPrefix(value, slPfx) { - // Deserializing assumes overwrite - _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice) - i.hasBeenSet = true - return nil - } - - tmp, err := strconv.ParseInt(value, 0, 64) - if err != nil { - return err - } - - i.slice = append(i.slice, int(tmp)) - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *IntSlice) String() string { - return fmt.Sprintf("%#v", f.slice) -} - -// Serialized allows IntSlice to fulfill Serializeder -func (i *IntSlice) Serialized() string { - jsonBytes, _ := json.Marshal(i.slice) - return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) -} - -// Value returns the slice of ints set by this flag -func (i *IntSlice) Value() []int { - return i.slice -} - -// Get returns the slice of ints set by this flag -func (f *IntSlice) Get() interface{} { - return *f -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *IntSliceFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f *IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - newVal := NewIntSlice() - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - if err := newVal.Set(s); err != nil { - return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", envVal, f.Name, err) - } - } - f.Value = newVal - break - } - } - } - - if f.Value == nil { - f.Value = NewIntSlice() - } - - for _, name := range f.Names() { - set.Var(f.Value, name, f.Usage) - } - return nil -} - -// Int64Slice is an opaque type for []int to satisfy flag.Value -type Int64Slice struct { - slice []int64 - hasBeenSet bool -} - -// Set parses the value into an integer and appends it to the list of values -func (f *Int64Slice) Set(value string) error { - if !f.hasBeenSet { - f.slice = []int64{} - f.hasBeenSet = true - } - - if strings.HasPrefix(value, slPfx) { - // Deserializing assumes overwrite - _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &f.slice) - f.hasBeenSet = true - return nil - } - - tmp, err := strconv.ParseInt(value, 0, 64) - if err != nil { - return err - } - - f.slice = append(f.slice, tmp) - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *Int64Slice) String() string { - return fmt.Sprintf("%#v", f.slice) -} - -// Serialized allows Int64Slice to fulfill Serializeder -func (f *Int64Slice) Serialized() string { - jsonBytes, _ := json.Marshal(f.slice) - return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) -} - -// Value returns the slice of ints set by this flag -func (f *Int64Slice) Value() []int64 { - return f.slice -} - -// Get returns the slice of ints set by this flag -func (f *Int64Slice) Get() interface{} { - return *f -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *Int64SliceFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f *Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - newVal := NewInt64Slice() - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - if err := newVal.Set(s); err != nil { - return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", envVal, f.Name, err) - } - } - f.Value = newVal - break - } - } - } - - if f.Value == nil { - f.Value = NewInt64Slice() - } - - for _, name := range f.Names() { - set.Var(f.Value, name, f.Usage) - } - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *BoolFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f *BoolFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - if envVal == "" { - f.Value = false - break - } - - envValBool, err := strconv.ParseBool(envVal) - if err != nil { - return fmt.Errorf("could not parse %q as bool value for flag %s: %s", envVal, f.Name, err) - } - f.Value = envValBool - break - } - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.BoolVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.Bool(name, f.Value, f.Usage) - } - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *StringFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f *StringFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - f.Value = envVal - break - } - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.StringVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.String(name, f.Value, f.Usage) - } - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *PathFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f *PathFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - f.Value = envVal - break - } - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.StringVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.String(name, f.Value, f.Usage) - } - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *IntFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f *IntFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %q as int value for flag %s: %s", envVal, f.Name, err) - } - f.Value = int(envValInt) - break - } - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.IntVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.Int(name, f.Value, f.Usage) - } - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *Int64Flag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f *Int64Flag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %q as int value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = envValInt - break - } - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.Int64Var(f.Destination, name, f.Value, f.Usage) - return nil - } - set.Int64(name, f.Value, f.Usage) - } - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *UintFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f *UintFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - envValInt, err := strconv.ParseUint(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %q as uint value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = uint(envValInt) - break - } - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.UintVar(f.Destination, name, f.Value, f.Usage) - return nil - } - set.Uint(name, f.Value, f.Usage) - } - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *Uint64Flag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f *Uint64Flag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - envValInt, err := strconv.ParseUint(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %q as uint64 value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = uint64(envValInt) - break - } - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.Uint64Var(f.Destination, name, f.Value, f.Usage) - return nil - } - set.Uint64(name, f.Value, f.Usage) - } - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *DurationFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) + IsRequired() bool } -// ApplyWithError populates the flag given the flag set and environment -func (f *DurationFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - envValDuration, err := time.ParseDuration(envVal) - if err != nil { - return fmt.Errorf("could not parse %q as duration for flag %s: %s", envVal, f.Name, err) - } +// DocGenerationFlag is an interface that allows documentation generation for the flag +type DocGenerationFlag interface { + Flag - f.Value = envValDuration - break - } - } - } + // TakesValue returns true of the flag takes a value, otherwise false + TakesValue() bool - for _, name := range f.Names() { - if f.Destination != nil { - set.DurationVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.Duration(name, f.Value, f.Usage) - } - return nil -} + // GetUsage returns the usage string for the flag + GetUsage() string -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *Float64Flag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) + // GetValue returns the flags value as string representation and an empty + // string if the flag takes no value at all. + GetValue() string } -// ApplyWithError populates the flag given the flag set and environment -func (f *Float64Flag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - envValFloat, err := strconv.ParseFloat(envVal, 10) - if err != nil { - return fmt.Errorf("could not parse %q as float64 value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = float64(envValFloat) - break - } - } - } +func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { + set := flag.NewFlagSet(name, flag.ContinueOnError) - for _, name := range f.Names() { - if f.Destination != nil { - set.Float64Var(f.Destination, name, f.Value, f.Usage) - continue + for _, f := range flags { + if err := f.Apply(set); err != nil { + return nil, err } - set.Float64(name, f.Value, f.Usage) - } - return nil -} - -// NewFloat64Slice makes a *Float64Slice with default values -func NewFloat64Slice(defaults ...float64) *Float64Slice { - return &Float64Slice{slice: append([]float64{}, defaults...)} -} - -// Float64Slice is an opaque type for []float64 to satisfy flag.Value -type Float64Slice struct { - slice []float64 - hasBeenSet bool -} - -// Set parses the value into a float64 and appends it to the list of values -func (f *Float64Slice) Set(value string) error { - if !f.hasBeenSet { - f.slice = []float64{} - f.hasBeenSet = true } - - if strings.HasPrefix(value, slPfx) { - // Deserializing assumes overwrite - _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &f.slice) - f.hasBeenSet = true - return nil - } - - tmp, err := strconv.ParseFloat(value, 64) - if err != nil { - return err - } - - f.slice = append(f.slice, tmp) - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *Float64Slice) String() string { - return fmt.Sprintf("%#v", f.slice) -} - -// Serialized allows Float64Slice to fulfill Serializeder -func (f *Float64Slice) Serialized() string { - jsonBytes, _ := json.Marshal(f.slice) - return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) -} - -// Value returns the slice of float64s set by this flag -func (f *Float64Slice) Value() []float64 { - return f.slice -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f *Float64SliceFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) + set.SetOutput(ioutil.Discard) + return set, nil } -// ApplyWithError populates the flag given the flag set and environment -func (f *Float64SliceFlag) ApplyWithError(set *flag.FlagSet) error { - if f.EnvVars != nil { - for _, envVar := range f.EnvVars { - if envVal, ok := syscall.Getenv(envVar); ok { - newVal := NewFloat64Slice() - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - err := newVal.Set(s) - if err != nil { - fmt.Fprintf(ErrWriter, err.Error()) - } - } - f.Value = newVal - break - } - } - } - - if f.Value == nil { - f.Value = NewFloat64Slice() +func eachName(longName string, fn func(string)) { + parts := strings.Split(longName, ",") + for _, name := range parts { + name = strings.Trim(name, " ") + fn(name) } - - for _, name := range f.Names() { - set.Var(f.Value, name, f.Usage) - } - return nil } func visibleFlags(fl []Flag) []Flag { - visible := []Flag{} - for _, flag := range fl { - field := flagValue(flag).FieldByName("Hidden") + var visible []Flag + for _, f := range fl { + field := flagValue(f).FieldByName("Hidden") if !field.IsValid() || !field.Bool() { - visible = append(visible, flag) + visible = append(visible, f) } } return visible @@ -858,6 +226,7 @@ func withEnvHint(envVars []string, str string) string { suffix = "%" sep = "%, %" } + envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(envVars, sep), suffix) } return str + envText @@ -902,6 +271,14 @@ func flagStringField(f Flag, name string) string { return "" } +func withFileHint(filePath, str string) string { + fileText := "" + if filePath != "" { + fileText = fmt.Sprintf(" [%s]", filePath) + } + return str + fileText +} + func flagValue(f Flag) reflect.Value { fv := reflect.ValueOf(f) for fv.Kind() == reflect.Ptr { @@ -956,14 +333,14 @@ func stringifyFlag(f Flag) string { placeholder = defaultPlaceholder } - usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultValueString)) + usageWithDefault := strings.TrimSpace(usage + defaultValueString) return withEnvHint(flagStringSliceField(f, "EnvVars"), fmt.Sprintf("%s\t%s", prefixedNames(f.Names(), placeholder), usageWithDefault)) } func stringifyIntSliceFlag(f *IntSliceFlag) string { - defaultVals := []string{} + var defaultVals []string if f.Value != nil && len(f.Value.Value()) > 0 { for _, i := range f.Value.Value() { defaultVals = append(defaultVals, fmt.Sprintf("%d", i)) @@ -974,10 +351,10 @@ func stringifyIntSliceFlag(f *IntSliceFlag) string { } func stringifyInt64SliceFlag(f *Int64SliceFlag) string { - defaultVals := []string{} + var defaultVals []string if f.Value != nil && len(f.Value.Value()) > 0 { for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, fmt.Sprintf("%d", i)) + defaultVals = append(defaultVals, strconv.FormatInt(i, 10)) } } @@ -985,7 +362,8 @@ func stringifyInt64SliceFlag(f *Int64SliceFlag) string { } func stringifyFloat64SliceFlag(f *Float64SliceFlag) string { - defaultVals := []string{} + var defaultVals []string + if f.Value != nil && len(f.Value.Value()) > 0 { for _, i := range f.Value.Value() { defaultVals = append(defaultVals, strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", i), "0"), ".")) @@ -996,11 +374,12 @@ func stringifyFloat64SliceFlag(f *Float64SliceFlag) string { } func stringifyStringSliceFlag(f *StringSliceFlag) string { - defaultVals := []string{} + var defaultVals []string + if f.Value != nil && len(f.Value.Value()) > 0 { for _, s := range f.Value.Value() { if len(s) > 0 { - defaultVals = append(defaultVals, fmt.Sprintf("%q", s)) + defaultVals = append(defaultVals, strconv.Quote(s)) } } } @@ -1032,3 +411,18 @@ func hasFlag(flags []Flag, fl Flag) bool { return false } + +func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool) { + for _, envVar := range envVars { + envVar = strings.TrimSpace(envVar) + if val, ok := syscall.Getenv(envVar); ok { + return val, true + } + } + for _, fileVar := range strings.Split(filePath, ",") { + if data, err := ioutil.ReadFile(fileVar); err == nil { + return string(data), true + } + } + return "", false +} diff --git a/flag_bool.go b/flag_bool.go new file mode 100644 index 0000000..665a8ac --- /dev/null +++ b/flag_bool.go @@ -0,0 +1,107 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// BoolFlag is a flag with type bool +type BoolFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value bool + DefaultText string + Destination *bool +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *BoolFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *BoolFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *BoolFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *BoolFlag) TakesValue() bool { + return false +} + +// GetUsage returns the usage string for the flag +func (f *BoolFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *BoolFlag) GetValue() string { + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *BoolFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valBool, err := strconv.ParseBool(val) + if err != nil { + return fmt.Errorf("could not parse %s as bool value for flag %s: %s", val, f.Name, err) + } + f.Value = valBool + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.BoolVar(f.Destination, name, f.Value, f.Usage) + continue + } + set.Bool(name, f.Value, f.Usage) + } + + return nil +} + +// Bool looks up the value of a local BoolFlag, returns +// false if not found +func (c *Context) Bool(name string) bool { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupBool(name, fs) + } + return false +} + +// GlobalBool looks up the value of a global BoolFlag, returns +// false if not found +//func (c *Context) GlobalBool(name string) bool { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupBool(name, fs) +// } +// return false +//} + +// TODO: Fix Duplicate +func lookupBool(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return false + } + return parsed + } + return false +} diff --git a/flag_duration.go b/flag_duration.go new file mode 100644 index 0000000..e3ccd18 --- /dev/null +++ b/flag_duration.go @@ -0,0 +1,104 @@ +package cli + +import ( + "flag" + "fmt" + "time" +) + +// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) +type DurationFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value time.Duration + DefaultText string + Destination *time.Duration +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *DurationFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *DurationFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *DurationFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *DurationFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *DurationFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *DurationFlag) GetValue() string { + return f.Value.String() +} + +// Apply populates the flag given the flag set and environment +func (f *DurationFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != ""{ + valDuration, err := time.ParseDuration(val) + if err != nil { + return fmt.Errorf("could not parse %s as duration value for flag %s: %s", val, f.Name, err) + } + f.Value = valDuration + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.DurationVar(f.Destination, name, f.Value, f.Usage) + continue + } + set.Duration(name, f.Value, f.Usage) + } + return nil +} + +// Duration looks up the value of a local DurationFlag, returns +// 0 if not found +func (c *Context) Duration(name string) time.Duration { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupDuration(name, fs) + } + return 0 +} +// GlobalDuration looks up the value of a global DurationFlag, returns +// 0 if not found +//func (c *Context) GlobalDuration(name string) time.Duration { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupDuration(name, fs) +// } +// return 0 +//} + +func lookupDuration(name string, set *flag.FlagSet) time.Duration { + f := set.Lookup(name) + if f != nil { + parsed, err := time.ParseDuration(f.Value.String()) + if err != nil { + return 0 + } + return parsed + } + return 0 +} diff --git a/flag_float64.go b/flag_float64.go new file mode 100644 index 0000000..641cb0b --- /dev/null +++ b/flag_float64.go @@ -0,0 +1,106 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// Float64Flag is a flag with type float64 +type Float64Flag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value float64 + DefaultText string + Destination *float64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Float64Flag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *Float64Flag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *Float64Flag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *Float64Flag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *Float64Flag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Float64Flag) GetValue() string { + return fmt.Sprintf("%f", f.Value) +} + +// Apply populates the flag given the flag set and environment +func (f *Float64Flag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valFloat, err := strconv.ParseFloat(val, 10) + if err != nil { + return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", val, f.Name, err) + } + f.Value = valFloat + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.Float64Var(f.Destination, name, f.Value, f.Usage) + continue + } + set.Float64(name, f.Value, f.Usage) + } + + return nil +} + +// Float64 looks up the value of a local Float64Flag, returns +// 0 if not found +func (c *Context) Float64(name string) float64 { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupFloat64(name, fs) + } + return 0 +} + +// GlobalFloat64 looks up the value of a global Float64Flag, returns +// 0 if not found +//func (c *Context) GlobalFloat64(name string) float64 { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupFloat64(name, fs) +// } +// return 0 +//} + +func lookupFloat64(name string, set *flag.FlagSet) float64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseFloat(f.Value.String(), 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} diff --git a/flag_float64_slice.go b/flag_float64_slice.go new file mode 100644 index 0000000..8331c43 --- /dev/null +++ b/flag_float64_slice.go @@ -0,0 +1,164 @@ +package cli + +import ( + "encoding/json" + "flag" + "fmt" + "strconv" + "strings" +) + +// Float64Slice wraps []float64 to satisfy flag.Value +type Float64Slice struct { + slice []float64 + hasBeenSet bool +} + +// NewFloat64Slice makes a *Float64Slice with default values +func NewFloat64Slice(defaults ...float64) *Float64Slice { + return &Float64Slice{slice: append([]float64{}, defaults...)} +} + +// Set parses the value into a float64 and appends it to the list of values +func (f *Float64Slice) Set(value string) error { + if !f.hasBeenSet { + f.slice = []float64{} + f.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &f.slice) + f.hasBeenSet = true + return nil + } + + tmp, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + + f.slice = append(f.slice, tmp) + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (f *Float64Slice) String() string { + return fmt.Sprintf("%#v", f.slice) +} + +// Serialize allows Float64Slice to fulfill Serializer +func (f *Float64Slice) Serialize() string { + jsonBytes, _ := json.Marshal(f.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of float64s set by this flag +func (f *Float64Slice) Value() []float64 { + return f.slice +} + +// Get returns the slice of float64s set by this flag +func (f *Float64Slice) Get() interface{} { + return *f +} + +// Float64SliceFlag is a flag with type *Float64Slice +type Float64SliceFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value *Float64Slice + DefaultText string +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Float64SliceFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *Float64SliceFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *Float64SliceFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true if the flag takes a value, otherwise false +func (f *Float64SliceFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *Float64SliceFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Float64SliceFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + f.Value = &Float64Slice{} + + for _, s := range strings.Split(val, ",") { + if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %s as float64 slice value for flag %s: %s", f.Value, f.Name, err) + } + } + } + + for _, name := range f.Names() { + if f.Value == nil { + f.Value = &Float64Slice{} + } + set.Var(f.Value, name, f.Usage) + } + + return nil +} + +// Float64Slice looks up the value of a local Float64SliceFlag, returns +// nil if not found +func (c *Context) Float64Slice(name string) []float64 { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupFloat64Slice(name, fs) + } + return nil +} + +// GlobalFloat64Slice looks up the value of a global Float64SliceFlag, returns +// nil if not found +//func (c *Context) GlobalFloat64Slice(name string) []int { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupFloat64Slice(name, fs) +// } +// return nil +//} + +func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*Float64Slice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} diff --git a/flag_generated.go b/flag_generated.go deleted file mode 100644 index a958150..0000000 --- a/flag_generated.go +++ /dev/null @@ -1,629 +0,0 @@ -package cli - -import ( - "flag" - "strconv" - "time" -) - -// WARNING: This file is generated! - -// BoolFlag is a flag with type bool -type BoolFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value bool - DefaultText string - - Destination *bool -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *BoolFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *BoolFlag) Names() []string { - return flagNames(f) -} - -// Bool looks up the value of a local BoolFlag, returns -// false if not found -func (c *Context) Bool(name string) bool { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupBool(name, fs) - } - return false -} - -func lookupBool(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return false - } - return parsed - } - return false -} - -// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) -type DurationFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value time.Duration - DefaultText string - - Destination *time.Duration -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *DurationFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *DurationFlag) Names() []string { - return flagNames(f) -} - -// Duration looks up the value of a local DurationFlag, returns -// 0 if not found -func (c *Context) Duration(name string) time.Duration { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupDuration(name, fs) - } - return 0 -} - -func lookupDuration(name string, set *flag.FlagSet) time.Duration { - f := set.Lookup(name) - if f != nil { - parsed, err := time.ParseDuration(f.Value.String()) - if err != nil { - return 0 - } - return parsed - } - return 0 -} - -// Float64Flag is a flag with type float64 -type Float64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value float64 - DefaultText string - - Destination *float64 -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Float64Flag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Float64Flag) Names() []string { - return flagNames(f) -} - -// Float64 looks up the value of a local Float64Flag, returns -// 0 if not found -func (c *Context) Float64(name string) float64 { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupFloat64(name, fs) - } - return 0 -} - -func lookupFloat64(name string, set *flag.FlagSet) float64 { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseFloat(f.Value.String(), 64) - if err != nil { - return 0 - } - return parsed - } - return 0 -} - -// GenericFlag is a flag with type Generic -type GenericFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value Generic - DefaultText string -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *GenericFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *GenericFlag) Names() []string { - return flagNames(f) -} - -// Generic looks up the value of a local GenericFlag, returns -// nil if not found -func (c *Context) Generic(name string) interface{} { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupGeneric(name, fs) - } - return nil -} - -func lookupGeneric(name string, set *flag.FlagSet) interface{} { - f := set.Lookup(name) - if f != nil { - parsed, err := f.Value, error(nil) - if err != nil { - return nil - } - return parsed - } - return nil -} - -// Int64Flag is a flag with type int64 -type Int64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value int64 - DefaultText string - - Destination *int64 -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Int64Flag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Int64Flag) Names() []string { - return flagNames(f) -} - -// Int64 looks up the value of a local Int64Flag, returns -// 0 if not found -func (c *Context) Int64(name string) int64 { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupInt64(name, fs) - } - return 0 -} - -func lookupInt64(name string, set *flag.FlagSet) int64 { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return parsed - } - return 0 -} - -// IntFlag is a flag with type int -type IntFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value int - DefaultText string - - Destination *int -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *IntFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *IntFlag) Names() []string { - return flagNames(f) -} - -// Int looks up the value of a local IntFlag, returns -// 0 if not found -func (c *Context) Int(name string) int { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupInt(name, fs) - } - return 0 -} - -func lookupInt(name string, set *flag.FlagSet) int { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return int(parsed) - } - return 0 -} - -// IntSliceFlag is a flag with type *IntSlice -type IntSliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value *IntSlice - DefaultText string -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *IntSliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *IntSliceFlag) Names() []string { - return flagNames(f) -} - -// IntSlice looks up the value of a local IntSliceFlag, returns -// nil if not found -func (c *Context) IntSlice(name string) []int { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupIntSlice(name, fs) - } - return nil -} - -func lookupIntSlice(name string, set *flag.FlagSet) []int { - f := set.Lookup(name) - if f != nil { - parsed, err := (f.Value.(*IntSlice)).Value(), error(nil) - if err != nil { - return nil - } - return parsed - } - return nil -} - -// Int64SliceFlag is a flag with type *Int64Slice -type Int64SliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value *Int64Slice - DefaultText string -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Int64SliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Int64SliceFlag) Names() []string { - return flagNames(f) -} - -// Int64Slice looks up the value of a local Int64SliceFlag, returns -// nil if not found -func (c *Context) Int64Slice(name string) []int64 { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupInt64Slice(name, fs) - } - return nil -} - -func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { - f := set.Lookup(name) - if f != nil { - parsed, err := (f.Value.(*Int64Slice)).Value(), error(nil) - if err != nil { - return nil - } - return parsed - } - return nil -} - -// Float64SliceFlag is a flag with type *Float64Slice -type Float64SliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value *Float64Slice - DefaultText string -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Float64SliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Float64SliceFlag) Names() []string { - return flagNames(f) -} - -// Float64Slice looks up the value of a local Float64SliceFlag, returns -// nil if not found -func (c *Context) Float64Slice(name string) []float64 { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupFloat64Slice(name, fs) - } - return nil -} - -func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 { - f := set.Lookup(name) - if f != nil { - parsed, err := (f.Value.(*Float64Slice)).Value(), error(nil) - if err != nil { - return nil - } - return parsed - } - return nil -} - -// StringFlag is a flag with type string -type StringFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value string - DefaultText string - - Destination *string -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *StringFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *StringFlag) Names() []string { - return flagNames(f) -} - -// String looks up the value of a local StringFlag, returns -// "" if not found -func (c *Context) String(name string) string { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupString(name, fs) - } - return "" -} - -func lookupString(name string, set *flag.FlagSet) string { - f := set.Lookup(name) - if f != nil { - parsed, err := f.Value.String(), error(nil) - if err != nil { - return "" - } - return parsed - } - return "" -} - -// PathFlag is a flag with type string -type PathFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value string - DefaultText string - - Destination *string -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *PathFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *PathFlag) Names() []string { - return flagNames(f) -} - -// Path looks up the value of a local PathFlag, returns -// "" if not found -func (c *Context) Path(name string) string { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupPath(name, fs) - } - return "" -} - -func lookupPath(name string, set *flag.FlagSet) string { - f := set.Lookup(name) - if f != nil { - parsed, err := f.Value.String(), error(nil) - if err != nil { - return "" - } - return parsed - } - return "" -} - -// StringSliceFlag is a flag with type *StringSlice -type StringSliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value *StringSlice - DefaultText string -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *StringSliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *StringSliceFlag) Names() []string { - return flagNames(f) -} - -// StringSlice looks up the value of a local StringSliceFlag, returns -// nil if not found -func (c *Context) StringSlice(name string) []string { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupStringSlice(name, fs) - } - return nil -} - -func lookupStringSlice(name string, set *flag.FlagSet) []string { - f := set.Lookup(name) - if f != nil { - parsed, err := (f.Value.(*StringSlice)).Value(), error(nil) - if err != nil { - return nil - } - return parsed - } - return nil -} - -// Uint64Flag is a flag with type uint64 -type Uint64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value uint64 - DefaultText string - - Destination *uint64 -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Uint64Flag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Uint64Flag) Names() []string { - return flagNames(f) -} - -// Uint64 looks up the value of a local Uint64Flag, returns -// 0 if not found -func (c *Context) Uint64(name string) uint64 { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupUint64(name, fs) - } - return 0 -} - -func lookupUint64(name string, set *flag.FlagSet) uint64 { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return parsed - } - return 0 -} - -// UintFlag is a flag with type uint -type UintFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value uint - DefaultText string - - Destination *uint -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *UintFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *UintFlag) Names() []string { - return flagNames(f) -} - -// Uint looks up the value of a local UintFlag, returns -// 0 if not found -func (c *Context) Uint(name string) uint { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupUint(name, fs) - } - return 0 -} - -func lookupUint(name string, set *flag.FlagSet) uint { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return uint(parsed) - } - return 0 -} diff --git a/flag_generic.go b/flag_generic.go new file mode 100644 index 0000000..169c364 --- /dev/null +++ b/flag_generic.go @@ -0,0 +1,104 @@ +package cli + +import ( + "flag" + "fmt" +) + +// Generic is a generic parseable type identified by a specific flag +type Generic interface { + Set(value string) error + String() string +} + +// GenericFlag is a flag with type Generic +type GenericFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + TakesFile bool + Value Generic + DefaultText string +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *GenericFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *GenericFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *GenericFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *GenericFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *GenericFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *GenericFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// Apply takes the flagset and calls Set on the generic flag with the value +// provided by the user for parsing by the flag +func (f GenericFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if err := f.Value.Set(val); err != nil { + return fmt.Errorf("could not parse %s as value for flag %s: %s", val, f.Name, err) + } + } + + for _, name := range f.Names() { + set.Var(f.Value, name, f.Usage) + } + + return nil +} + +// Generic looks up the value of a local GenericFlag, returns +// nil if not found +func (c *Context) Generic(name string) interface{} { + return lookupGeneric(name, c.flagSet) +} + +// GlobalGeneric looks up the value of a global GenericFlag, returns +// nil if not found +//func (c *Context) GlobalGeneric(name string) interface{} { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupGeneric(name, fs) +// } +// return nil +//} + +func lookupGeneric(name string, set *flag.FlagSet) interface{} { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value, error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} diff --git a/flag_int.go b/flag_int.go new file mode 100644 index 0000000..f026650 --- /dev/null +++ b/flag_int.go @@ -0,0 +1,103 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// IntFlag is a flag with type int +type IntFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value int + DefaultText string + Destination *int +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *IntFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *IntFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *IntFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *IntFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *IntFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *IntFlag) GetValue() string { + return fmt.Sprintf("%d", f.Value) +} + +// Apply populates the flag given the flag set and environment +func (f *IntFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valInt, err := strconv.ParseInt(val, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as int value for flag %s: %s", val, f.Name, err) + } + f.Value = int(valInt) + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.IntVar(f.Destination, name, f.Value, f.Usage) + continue + } + set.Int(name, f.Value, f.Usage) + } + + return nil +} + +// Int looks up the value of a local IntFlag, returns +// 0 if not found +func (c *Context) Int(name string) int { + return lookupInt(name, c.flagSet) +} + +// GlobalInt looks up the value of a global IntFlag, returns +// 0 if not found +//func (c *Context) GlobalInt(name string) int { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupInt(name, fs) +// } +// return 0 +//} + +func lookupInt(name string, set *flag.FlagSet) int { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return int(parsed) + } + return 0 +} diff --git a/flag_int64.go b/flag_int64.go new file mode 100644 index 0000000..60ef026 --- /dev/null +++ b/flag_int64.go @@ -0,0 +1,103 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// Int64Flag is a flag with type int64 +type Int64Flag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value int64 + DefaultText string + Destination *int64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Int64Flag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *Int64Flag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *Int64Flag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *Int64Flag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *Int64Flag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Int64Flag) GetValue() string { + return fmt.Sprintf("%d", f.Value) +} + +// Apply populates the flag given the flag set and environment +func (f *Int64Flag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valInt, err := strconv.ParseInt(val, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as int value for flag %s: %s", val, f.Name, err) + } + f.Value = valInt + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.Int64Var(f.Destination, name, f.Value, f.Usage) + continue + } + set.Int64(name, f.Value, f.Usage) + } + + return nil +} + +// Int64 looks up the value of a local Int64Flag, returns +// 0 if not found +func (c *Context) Int64(name string) int64 { + return lookupInt64(name, c.flagSet) +} + +// GlobalInt64 looks up the value of a global Int64Flag, returns +// 0 if not found +//func (c *Context) GlobalInt64(name string) int64 { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupInt64(name, fs) +// } +// return 0 +//} + +func lookupInt64(name string, set *flag.FlagSet) int64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} diff --git a/flag_int64_slice.go b/flag_int64_slice.go new file mode 100644 index 0000000..2c82329 --- /dev/null +++ b/flag_int64_slice.go @@ -0,0 +1,164 @@ +package cli + +import ( + "encoding/json" + "flag" + "fmt" + "strconv" + "strings" +) + +// Int64Slice wraps []int64 to satisfy flag.Value +type Int64Slice struct { + slice []int64 + hasBeenSet bool +} + +// NewInt64Slice makes an *Int64Slice with default values +func NewInt64Slice(defaults ...int64) *Int64Slice { + return &Int64Slice{slice: append([]int64{}, defaults...)} +} + +// Set parses the value into an integer and appends it to the list of values +func (i *Int64Slice) Set(value string) error { + if !i.hasBeenSet { + i.slice = []int64{} + i.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice) + i.hasBeenSet = true + return nil + } + + tmp, err := strconv.ParseInt(value, 0, 64) + if err != nil { + return err + } + + i.slice = append(i.slice, tmp) + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (i *Int64Slice) String() string { + return fmt.Sprintf("%#v", i.slice) +} + +// Serialize allows Int64Slice to fulfill Serializer +func (i *Int64Slice) Serialize() string { + jsonBytes, _ := json.Marshal(i.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of ints set by this flag +func (i *Int64Slice) Value() []int64 { + return i.slice +} + +// Get returns the slice of ints set by this flag +func (i *Int64Slice) Get() interface{} { + return *i +} + +// Int64SliceFlag is a flag with type *Int64Slice +type Int64SliceFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value *Int64Slice + DefaultText string +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Int64SliceFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *Int64SliceFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *Int64SliceFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *Int64SliceFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f Int64SliceFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Int64SliceFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + f.Value = &Int64Slice{} + + for _, s := range strings.Split(val, ",") { + if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", f.Value, f.Name, err) + } + } + } + + for _, name := range f.Names() { + if f.Value == nil { + f.Value = &Int64Slice{} + } + set.Var(f.Value, name, f.Usage) + } + + return nil +} + +// Int64Slice looks up the value of a local Int64SliceFlag, returns +// nil if not found +func (c *Context) Int64Slice(name string) []int64 { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupInt64Slice(name, fs) + } + return nil +} + +// GlobalInt64Slice looks up the value of a global Int64SliceFlag, returns +// nil if not found +//func (c *Context) GlobalInt64Slice(name string) []int { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupInt64Slice(name, fs) +// } +// return nil +//} + +func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*Int64Slice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} diff --git a/flag_int_slice.go b/flag_int_slice.go new file mode 100644 index 0000000..c885dd3 --- /dev/null +++ b/flag_int_slice.go @@ -0,0 +1,175 @@ +package cli + +import ( + "encoding/json" + "flag" + "fmt" + "strconv" + "strings" +) + +// IntSlice wraps []int to satisfy flag.Value +type IntSlice struct { + slice []int + hasBeenSet bool +} + +// NewIntSlice makes an *IntSlice with default values +func NewIntSlice(defaults ...int) *IntSlice { + return &IntSlice{slice: append([]int{}, defaults...)} +} + +// TODO: Consistently have specific Set function for Int64 and Float64 ? +// SetInt directly adds an integer to the list of values +func (i *IntSlice) SetInt(value int) { + if !i.hasBeenSet { + i.slice = []int{} + i.hasBeenSet = true + } + + i.slice = append(i.slice, value) +} + +// Set parses the value into an integer and appends it to the list of values +func (i *IntSlice) Set(value string) error { + if !i.hasBeenSet { + i.slice = []int{} + i.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice) + i.hasBeenSet = true + return nil + } + + tmp, err := strconv.ParseInt(value, 0, 64) + if err != nil { + return err + } + + i.slice = append(i.slice, int(tmp)) + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (i *IntSlice) String() string { + return fmt.Sprintf("%#v", i.slice) +} + +// Serialize allows IntSlice to fulfill Serializer +func (i *IntSlice) Serialize() string { + jsonBytes, _ := json.Marshal(i.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of ints set by this flag +func (i *IntSlice) Value() []int { + return i.slice +} + +// Get returns the slice of ints set by this flag +func (i *IntSlice) Get() interface{} { + return *i +} + +// IntSliceFlag is a flag with type *IntSlice +type IntSliceFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value *IntSlice + DefaultText string +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *IntSliceFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *IntSliceFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *IntSliceFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *IntSliceFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f IntSliceFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *IntSliceFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + f.Value = &IntSlice{} + + for _, s := range strings.Split(val, ",") { + if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", val, f.Name, err) + } + } + } + + for _, name := range f.Names() { + if f.Value == nil { + f.Value = &IntSlice{} + } + set.Var(f.Value, name, f.Usage) + } + + return nil +} + +// IntSlice looks up the value of a local IntSliceFlag, returns +// nil if not found +func (c *Context) IntSlice(name string) []int { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupIntSlice(name, c.flagSet) + } + return nil +} + +// GlobalIntSlice looks up the value of a global IntSliceFlag, returns +// nil if not found +//func (c *Context) GlobalIntSlice(name string) []int { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupIntSlice(name, fs) +// } +// return nil +//} + +func lookupIntSlice(name string, set *flag.FlagSet) []int { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*IntSlice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} diff --git a/flag_path.go b/flag_path.go new file mode 100644 index 0000000..0e7fd01 --- /dev/null +++ b/flag_path.go @@ -0,0 +1,85 @@ +package cli + +import "flag" + +type PathFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + EnvVar string + FilePath string + Required bool + Hidden bool + TakesFile bool + Value string + DefaultText string + Destination *string +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *PathFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *PathFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *PathFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *PathFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *PathFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *PathFlag) GetValue() string { + return f.Value +} + +// Apply populates the flag given the flag set and environment +func (f *PathFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + f.Value = val + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.StringVar(f.Destination, name, f.Value, f.Usage) + return + } + set.String(name, f.Value, f.Usage) + } + + return nil +} + +// String looks up the value of a local PathFlag, returns +// "" if not found +func (c *Context) Path(name string) string { + return lookupPath(name, c.flagSet) +} + +func lookupPath(name string, set *flag.FlagSet) string { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value.String(), error(nil) + if err != nil { + return "" + } + return parsed + } + return "" +} diff --git a/flag_string.go b/flag_string.go new file mode 100644 index 0000000..fccc670 --- /dev/null +++ b/flag_string.go @@ -0,0 +1,95 @@ +package cli + +import "flag" + +// StringFlag is a flag with type string +type StringFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + EnvVar string + FilePath string + Required bool + Hidden bool + TakesFile bool + Value string + DefaultText string + Destination *string +} + +// String returns a readable representation of this value +// (for usage defaults) +func (s *StringFlag) String() string { + return FlagStringer(s) +} + +// Names returns the names of the flag +func (s *StringFlag) Names() []string { + return flagNames(s) +} + +// IsRequired returns whether or not the flag is required +func (s *StringFlag) IsRequired() bool { + return s.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (s *StringFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (s *StringFlag) GetUsage() string { + return s.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (s *StringFlag) GetValue() string { + return s.Value +} + +// Apply populates the flag given the flag set and environment +func (s *StringFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(s.EnvVars, s.FilePath); ok { + s.Value = val + } + + for _, name := range s.Names() { + if s.Destination != nil { + set.StringVar(s.Destination, name, s.Value, s.Usage) + return + } + set.String(name, s.Value, s.Usage) + } + + return nil +} + +// String looks up the value of a local StringFlag, returns +// "" if not found +func (c *Context) String(name string) string { + return lookupString(name, c.flagSet) +} + +// GlobalString looks up the value of a global StringFlag, returns +// "" if not found +//func (c *Context) GlobalString(name string) string { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupPath(name, fs) +// } +// return "" +//} + +func lookupString(name string, set *flag.FlagSet) string { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value.String(), error(nil) + if err != nil { + return "" + } + return parsed + } + return "" +} diff --git a/flag_string_slice.go b/flag_string_slice.go new file mode 100644 index 0000000..604fab4 --- /dev/null +++ b/flag_string_slice.go @@ -0,0 +1,155 @@ +package cli + +import ( + "encoding/json" + "flag" + "fmt" + "strings" +) + +// StringSlice wraps a []string to satisfy flag.Value +type StringSlice struct { + slice []string + hasBeenSet bool +} + +// NewStringSlice creates a *StringSlice with default values +func NewStringSlice(defaults ...string) *StringSlice { + return &StringSlice{slice: append([]string{}, defaults...)} +} + +// Set appends the string value to the list of values +func (s *StringSlice) Set(value string) error { + if !s.hasBeenSet { + s.slice = []string{} + s.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &s.slice) + s.hasBeenSet = true + return nil + } + + s.slice = append(s.slice, value) + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (s *StringSlice) String() string { + return fmt.Sprintf("%s", s.slice) +} + +// Serialize allows StringSlice to fulfill Serializer +func (s *StringSlice) Serialize() string { + jsonBytes, _ := json.Marshal(s.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of strings set by this flag +func (s *StringSlice) Value() []string { + return s.slice +} + +// Get returns the slice of strings set by this flag +func (s *StringSlice) Get() interface{} { + return *s +} + +// StringSliceFlag is a flag with type *StringSlice +type StringSliceFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + TakesFile bool + Value *StringSlice + DefaultText string +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *StringSliceFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *StringSliceFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *StringSliceFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *StringSliceFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *StringSliceFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *StringSliceFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + f.Value = &StringSlice{} + for _, s := range strings.Split(val, ",") { + if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %s as string value for flag %s: %s", val, f.Name, err) + } + } + } + + for _, name := range f.Names() { + if f.Value == nil { + f.Value = &StringSlice{} + } + set.Var(f.Value, name, f.Usage) + } + + return nil +} + +// StringSlice looks up the value of a local StringSliceFlag, returns +// nil if not found +func (c *Context) StringSlice(name string) []string { + return lookupStringSlice(name, c.flagSet) +} + +// GlobalStringSlice looks up the value of a global StringSliceFlag, returns +// nil if not found +//func (c *Context) GlobalStringSlice(name string) []string { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupStringSlice(name, fs) +// } +// return nil +//} + +func lookupStringSlice(name string, set *flag.FlagSet) []string { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*StringSlice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} diff --git a/flag_test.go b/flag_test.go index 6ed3a76..d097776 100644 --- a/flag_test.go +++ b/flag_test.go @@ -3,6 +3,8 @@ package cli import ( "flag" "fmt" + "io" + "io/ioutil" "os" "reflect" "regexp" @@ -111,10 +113,16 @@ func TestFlagsFromEnv(t *testing.T) { {"foo,bar", &Parser{"foo", "bar"}, &GenericFlag{Name: "names", Value: &Parser{}, EnvVars: []string{"NAMES"}}, ""}, } +//<<<<<<< HEAD +// for i, test := range flagTests { +// os.Clearenv() +// envVarSlice := reflect.Indirect(reflect.ValueOf(test.flag)).FieldByName("EnvVars").Slice(0, 1) +// os.Setenv(envVarSlice.Index(0).String(), test.input) +//======= for i, test := range flagTests { - clearenv() - envVarSlice := reflect.Indirect(reflect.ValueOf(test.flag)).FieldByName("EnvVars").Slice(0, 1) - os.Setenv(envVarSlice.Index(0).String(), test.input) + os.Clearenv() + _ = os.Setenv(reflect.ValueOf(test.flag).FieldByName("EnvVar").String(), test.input) +//>>>>>>> master a := App{ Flags: []Flag{test.flag}, Action: func(ctx *Context) error { @@ -180,8 +188,10 @@ func TestStringFlagDefaultText(t *testing.T) { } func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() - os.Setenv("APP_FOO", "derp") + + os.Clearenv() + _ = os.Setenv("APP_FOO", "derp") + for _, test := range stringFlagTests { flag := &StringFlag{Name: test.name, Aliases: test.aliases, Value: test.value, EnvVars: []string{"APP_FOO"}} output := flag.String() @@ -196,6 +206,48 @@ func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { } } +var prefixStringFlagTests = []struct { + name string + usage string + value string + prefixer FlagNamePrefixFunc + expected string +}{ + {"foo", "", "", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: foo, ph: value\t"}, + {"f", "", "", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: f, ph: value\t"}, + {"f", "The total `foo` desired", "all", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: f, ph: foo\tThe total foo desired (default: \"all\")"}, + {"test", "", "Something", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: test, ph: value\t(default: \"Something\")"}, + {"config,c", "Load configuration from `FILE`", "", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: config,c, ph: FILE\tLoad configuration from FILE"}, + {"config,c", "Load configuration from `CONFIG`", "config.json", func(a, b string) string { + return fmt.Sprintf("name: %s, ph: %s", a, b) + }, "name: config,c, ph: CONFIG\tLoad configuration from CONFIG (default: \"config.json\")"}, +} + +func TestFlagNamePrefixer(t *testing.T) { + defer func() { + FlagNamePrefixer = prefixedNames + }() + + for _, test := range prefixStringFlagTests { + FlagNamePrefixer = test.prefixer + flag := StringFlag{Name: test.name, Usage: test.usage, Value: test.value} + output := flag.String() + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + func TestStringFlagApply_SetsAllNames(t *testing.T) { v := "mmm" fl := StringFlag{Name: "hay", Aliases: []string{"H", "hayyy"}, Destination: &v} @@ -223,6 +275,7 @@ func TestPathFlagHelpOutput(t *testing.T) { flag := &PathFlag{Name: test.name, Aliases: test.aliases, Usage: test.usage, Value: test.value} output := flag.String() + if output != test.expected { t.Errorf("%q does not match %q", output, test.expected) } @@ -230,7 +283,7 @@ func TestPathFlagHelpOutput(t *testing.T) { } func TestPathFlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() + os.Clearenv() os.Setenv("APP_PATH", "/path/to/file") for _, test := range pathFlagTests { flag := &PathFlag{Name: test.name, Aliases: test.aliases, Value: test.value, EnvVars: []string{"APP_PATH"}} @@ -257,17 +310,54 @@ func TestPathFlagApply_SetsAllNames(t *testing.T) { expect(t, v, "/path/to/file/PATH") } +var envHintFlagTests = []struct { + name string + env string + hinter FlagEnvHintFunc + expected string +}{ + {"foo", "", func(a, b string) string { + return fmt.Sprintf("env: %s, str: %s", a, b) + }, "env: , str: --foo value\t"}, + {"f", "", func(a, b string) string { + return fmt.Sprintf("env: %s, str: %s", a, b) + }, "env: , str: -f value\t"}, + {"foo", "ENV_VAR", func(a, b string) string { + return fmt.Sprintf("env: %s, str: %s", a, b) + }, "env: ENV_VAR, str: --foo value\t"}, + {"f", "ENV_VAR", func(a, b string) string { + return fmt.Sprintf("env: %s, str: %s", a, b) + }, "env: ENV_VAR, str: -f value\t"}, +} + +func TestFlagEnvHinter(t *testing.T) { + defer func() { + FlagEnvHinter = withEnvHint + }() + + for _, test := range envHintFlagTests { + FlagEnvHinter = test.hinter + flag := StringFlag{Name: test.name, EnvVar: test.env} + output := flag.String() + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + var stringSliceFlagTests = []struct { name string aliases []string value *StringSlice expected string }{ + {"foo", nil, NewStringSlice(""), "--foo value\t"}, {"f", nil, NewStringSlice(""), "-f value\t"}, {"f", nil, NewStringSlice("Lipstick"), "-f value\t(default: \"Lipstick\")"}, {"test", nil, NewStringSlice("Something"), "--test value\t(default: \"Something\")"}, {"dee", []string{"d"}, NewStringSlice("Inka", "Dinka", "dooo"), "--dee value, -d value\t(default: \"Inka\", \"Dinka\", \"dooo\")"}, + } func TestStringSliceFlagHelpOutput(t *testing.T) { @@ -282,8 +372,9 @@ func TestStringSliceFlagHelpOutput(t *testing.T) { } func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() - os.Setenv("APP_QWWX", "11,4") + os.Clearenv() + _ = os.Setenv("APP_QWWX", "11,4") + for _, test := range stringSliceFlagTests { flag := &StringSliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value, EnvVars: []string{"APP_QWWX"}} output := flag.String() @@ -327,8 +418,10 @@ func TestIntFlagHelpOutput(t *testing.T) { } func TestIntFlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() + os.Clearenv() os.Setenv("APP_BAR", "2") + _ = os.Setenv("APP_BAR", "2") + for _, test := range intFlagTests { flag := &IntFlag{Name: test.name, EnvVars: []string{"APP_BAR"}} output := flag.String() @@ -374,8 +467,9 @@ func TestInt64FlagHelpOutput(t *testing.T) { } func TestInt64FlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() - os.Setenv("APP_BAR", "2") + os.Clearenv() + _ = os.Setenv("APP_BAR", "2") + for _, test := range int64FlagTests { flag := IntFlag{Name: test.name, EnvVars: []string{"APP_BAR"}} output := flag.String() @@ -410,8 +504,9 @@ func TestUintFlagHelpOutput(t *testing.T) { } func TestUintFlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() - os.Setenv("APP_BAR", "2") + os.Clearenv() + _ = os.Setenv("APP_BAR", "2") + for _, test := range uintFlagTests { flag := UintFlag{Name: test.name, EnvVars: []string{"APP_BAR"}} output := flag.String() @@ -446,8 +541,9 @@ func TestUint64FlagHelpOutput(t *testing.T) { } func TestUint64FlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() - os.Setenv("APP_BAR", "2") + os.Clearenv() + _ = os.Setenv("APP_BAR", "2") + for _, test := range uint64FlagTests { flag := UintFlag{Name: test.name, EnvVars: []string{"APP_BAR"}} output := flag.String() @@ -482,8 +578,9 @@ func TestDurationFlagHelpOutput(t *testing.T) { } func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() - os.Setenv("APP_BAR", "2h3m6s") + os.Clearenv() + _ = os.Setenv("APP_BAR", "2h3m6s") + for _, test := range durationFlagTests { flag := &DurationFlag{Name: test.name, EnvVars: []string{"APP_BAR"}} output := flag.String() @@ -532,8 +629,9 @@ func TestIntSliceFlagHelpOutput(t *testing.T) { } func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() - os.Setenv("APP_SMURF", "42,3") + os.Clearenv() + _ = os.Setenv("APP_SMURF", "42,3") + for _, test := range intSliceFlagTests { flag := &IntSliceFlag{Name: test.name, Aliases: test.aliases, Value: test.value, EnvVars: []string{"APP_SMURF"}} output := flag.String() @@ -581,8 +679,9 @@ func TestInt64SliceFlagHelpOutput(t *testing.T) { } func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() - os.Setenv("APP_SMURF", "42,17179869184") + os.Clearenv() + _ = os.Setenv("APP_SMURF", "42,17179869184") + for _, test := range int64SliceFlagTests { flag := Int64SliceFlag{Name: test.name, Value: test.value, EnvVars: []string{"APP_SMURF"}} output := flag.String() @@ -607,8 +706,8 @@ var float64FlagTests = []struct { func TestFloat64FlagHelpOutput(t *testing.T) { for _, test := range float64FlagTests { - flag := &Float64Flag{Name: test.name, Value: float64(0.1)} - output := flag.String() + f := &Float64Flag{Name: test.name, Value: 0.1} + output := f.String() if output != test.expected { t.Errorf("%q does not match %q", output, test.expected) @@ -617,8 +716,9 @@ func TestFloat64FlagHelpOutput(t *testing.T) { } func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() - os.Setenv("APP_BAZ", "99.4") + os.Clearenv() + _ = os.Setenv("APP_BAZ", "99.4") + for _, test := range float64FlagTests { flag := &Float64Flag{Name: test.name, EnvVars: []string{"APP_BAZ"}} output := flag.String() @@ -634,7 +734,7 @@ func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) { } func TestFloat64FlagApply_SetsAllNames(t *testing.T) { - v := float64(99.1) + v := 99.1 fl := Float64Flag{Name: "noodles", Aliases: []string{"N", "nurbles"}, Destination: &v} set := flag.NewFlagSet("test", 0) fl.Apply(set) @@ -668,7 +768,7 @@ func TestFloat64SliceFlagHelpOutput(t *testing.T) { } func TestFloat64SliceFlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() + os.Clearenv() os.Setenv("APP_SMURF", "0.1234,-10.5") for _, test := range float64SliceFlagTests { flag := Float64SliceFlag{Name: test.name, Value: test.value, EnvVars: []string{"APP_SMURF"}} @@ -705,8 +805,9 @@ func TestGenericFlagHelpOutput(t *testing.T) { } func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) { - clearenv() - os.Setenv("APP_ZAP", "3") + os.Clearenv() + _ = os.Setenv("APP_ZAP", "3") + for _, test := range genericFlagTests { flag := &GenericFlag{Name: test.name, EnvVars: []string{"APP_ZAP"}} output := flag.String() @@ -731,7 +832,7 @@ func TestGenericFlagApply_SetsAllNames(t *testing.T) { } func TestParseMultiString(t *testing.T) { - (&App{ + _ = (&App{ Flags: []Flag{ &StringFlag{Name: "serve", Aliases: []string{"s"}}, }, @@ -749,7 +850,7 @@ func TestParseMultiString(t *testing.T) { func TestParseDestinationString(t *testing.T) { var dest string - a := App{ + _ = (&App{ Flags: []Flag{ &StringFlag{ Name: "dest", @@ -762,14 +863,13 @@ func TestParseDestinationString(t *testing.T) { } return nil }, - } - a.Run([]string{"run", "--dest", "10"}) + }).Run([]string{"run", "--dest", "10"}) } func TestParseMultiStringFromEnv(t *testing.T) { - clearenv() - os.Setenv("APP_COUNT", "20") - (&App{ + os.Clearenv() + _ = os.Setenv("APP_COUNT", "20") + _ = (&App{ Flags: []Flag{ &StringFlag{Name: "count", Aliases: []string{"c"}, EnvVars: []string{"APP_COUNT"}}, }, @@ -786,9 +886,9 @@ func TestParseMultiStringFromEnv(t *testing.T) { } func TestParseMultiStringFromEnvCascade(t *testing.T) { - clearenv() - os.Setenv("APP_COUNT", "20") - (&App{ + os.Clearenv() + _ = os.Setenv("APP_COUNT", "20") + _ = (&App{ Flags: []Flag{ &StringFlag{Name: "count", Aliases: []string{"c"}, EnvVars: []string{"COMPAT_COUNT", "APP_COUNT"}}, }, @@ -805,7 +905,7 @@ func TestParseMultiStringFromEnvCascade(t *testing.T) { } func TestParseMultiStringSlice(t *testing.T) { - (&App{ + _ = (&App{ Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: NewStringSlice()}, }, @@ -823,7 +923,7 @@ func TestParseMultiStringSlice(t *testing.T) { } func TestParseMultiStringSliceWithDefaults(t *testing.T) { - (&App{ + _ = (&App{ Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: NewStringSlice("9", "2")}, }, @@ -841,7 +941,7 @@ func TestParseMultiStringSliceWithDefaults(t *testing.T) { } func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) { - (&App{ + _ = (&App{ Flags: []Flag{ &StringSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: NewStringSlice("9", "2")}, }, @@ -858,10 +958,10 @@ func TestParseMultiStringSliceWithDefaultsUnset(t *testing.T) { } func TestParseMultiStringSliceFromEnv(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "20,30,40") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") - (&App{ + _ = (&App{ Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewStringSlice(), EnvVars: []string{"APP_INTERVALS"}}, }, @@ -878,10 +978,10 @@ func TestParseMultiStringSliceFromEnv(t *testing.T) { } func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "20,30,40") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") - (&App{ + _ = (&App{ Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewStringSlice("1", "2", "5"), EnvVars: []string{"APP_INTERVALS"}}, }, @@ -898,10 +998,10 @@ func TestParseMultiStringSliceFromEnvWithDefaults(t *testing.T) { } func TestParseMultiStringSliceFromEnvCascade(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "20,30,40") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") - (&App{ + _ = (&App{ Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewStringSlice(), EnvVars: []string{"COMPAT_INTERVALS", "APP_INTERVALS"}}, }, @@ -918,10 +1018,10 @@ func TestParseMultiStringSliceFromEnvCascade(t *testing.T) { } func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "20,30,40") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") - (&App{ + _ = (&App{ Flags: []Flag{ &StringSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewStringSlice("1", "2", "5"), EnvVars: []string{"COMPAT_INTERVALS", "APP_INTERVALS"}}, }, @@ -938,7 +1038,7 @@ func TestParseMultiStringSliceFromEnvCascadeWithDefaults(t *testing.T) { } func TestParseMultiInt(t *testing.T) { - a := App{ + _ = (&App{ Flags: []Flag{ &IntFlag{Name: "serve", Aliases: []string{"s"}}, }, @@ -951,13 +1051,12 @@ func TestParseMultiInt(t *testing.T) { } return nil }, - } - a.Run([]string{"run", "-s", "10"}) + }).Run([]string{"run", "-s", "10"}) } func TestParseDestinationInt(t *testing.T) { var dest int - a := App{ + _ = (&App{ Flags: []Flag{ &IntFlag{ Name: "dest", @@ -970,14 +1069,13 @@ func TestParseDestinationInt(t *testing.T) { } return nil }, - } - a.Run([]string{"run", "--dest", "10"}) + }).Run([]string{"run", "--dest", "10"}) } func TestParseMultiIntFromEnv(t *testing.T) { - clearenv() - os.Setenv("APP_TIMEOUT_SECONDS", "10") - a := App{ + os.Clearenv() + _ = os.Setenv("APP_TIMEOUT_SECONDS", "10") + _ = (&App{ Flags: []Flag{ &IntFlag{Name: "timeout", Aliases: []string{"t"}, EnvVars: []string{"APP_TIMEOUT_SECONDS"}}, }, @@ -990,14 +1088,13 @@ func TestParseMultiIntFromEnv(t *testing.T) { } return nil }, - } - a.Run([]string{"run"}) + }).Run([]string{"run"}) } func TestParseMultiIntFromEnvCascade(t *testing.T) { - clearenv() - os.Setenv("APP_TIMEOUT_SECONDS", "10") - a := App{ + os.Clearenv() + _ = os.Setenv("APP_TIMEOUT_SECONDS", "10") + _ = (&App{ Flags: []Flag{ &IntFlag{Name: "timeout", Aliases: []string{"t"}, EnvVars: []string{"COMPAT_TIMEOUT_SECONDS", "APP_TIMEOUT_SECONDS"}}, }, @@ -1010,12 +1107,11 @@ func TestParseMultiIntFromEnvCascade(t *testing.T) { } return nil }, - } - a.Run([]string{"run"}) + }).Run([]string{"run"}) } func TestParseMultiIntSlice(t *testing.T) { - (&App{ + _ = (&App{ Flags: []Flag{ &IntSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: NewIntSlice()}, }, @@ -1032,7 +1128,7 @@ func TestParseMultiIntSlice(t *testing.T) { } func TestParseMultiIntSliceWithDefaults(t *testing.T) { - (&App{ + _ = (&App{ Flags: []Flag{ &IntSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: NewIntSlice(9, 2)}, }, @@ -1049,7 +1145,7 @@ func TestParseMultiIntSliceWithDefaults(t *testing.T) { } func TestParseMultiIntSliceWithDefaultsUnset(t *testing.T) { - (&App{ + _ = (&App{ Flags: []Flag{ &IntSliceFlag{Name: "serve", Aliases: []string{"s"}, Value: NewIntSlice(9, 2)}, }, @@ -1066,10 +1162,10 @@ func TestParseMultiIntSliceWithDefaultsUnset(t *testing.T) { } func TestParseMultiIntSliceFromEnv(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "20,30,40") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") - (&App{ + _ = (&App{ Flags: []Flag{ &IntSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewIntSlice(), EnvVars: []string{"APP_INTERVALS"}}, }, @@ -1086,10 +1182,10 @@ func TestParseMultiIntSliceFromEnv(t *testing.T) { } func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "20,30,40") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") - (&App{ + _ = (&App{ Flags: []Flag{ &IntSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewIntSlice(1, 2, 5), EnvVars: []string{"APP_INTERVALS"}}, }, @@ -1106,10 +1202,10 @@ func TestParseMultiIntSliceFromEnvWithDefaults(t *testing.T) { } func TestParseMultiIntSliceFromEnvCascade(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "20,30,40") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,40") - (&App{ + _ = (&App{ Flags: []Flag{ &IntSliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewIntSlice(), EnvVars: []string{"COMPAT_INTERVALS", "APP_INTERVALS"}}, }, @@ -1126,7 +1222,7 @@ func TestParseMultiIntSliceFromEnvCascade(t *testing.T) { } func TestParseMultiInt64Slice(t *testing.T) { - (&App{ + _ = (&App{ Flags: []Flag{ &Int64SliceFlag{Name: "serve", Aliases: []string{"s"}, Value: NewInt64Slice()}, }, @@ -1143,10 +1239,10 @@ func TestParseMultiInt64Slice(t *testing.T) { } func TestParseMultiInt64SliceFromEnv(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "20,30,17179869184") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,17179869184") - (&App{ + _ = (&App{ Flags: []Flag{ &Int64SliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewInt64Slice(), EnvVars: []string{"APP_INTERVALS"}}, }, @@ -1163,10 +1259,10 @@ func TestParseMultiInt64SliceFromEnv(t *testing.T) { } func TestParseMultiInt64SliceFromEnvCascade(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "20,30,17179869184") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "20,30,17179869184") - (&App{ + _ = (&App{ Flags: []Flag{ &Int64SliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewInt64Slice(), EnvVars: []string{"COMPAT_INTERVALS", "APP_INTERVALS"}}, }, @@ -1183,7 +1279,7 @@ func TestParseMultiInt64SliceFromEnvCascade(t *testing.T) { } func TestParseMultiFloat64(t *testing.T) { - a := App{ + _ = (&App{ Flags: []Flag{ &Float64Flag{Name: "serve", Aliases: []string{"s"}}, }, @@ -1196,13 +1292,12 @@ func TestParseMultiFloat64(t *testing.T) { } return nil }, - } - a.Run([]string{"run", "-s", "10.2"}) + }).Run([]string{"run", "-s", "10.2"}) } func TestParseDestinationFloat64(t *testing.T) { var dest float64 - a := App{ + _ = (&App{ Flags: []Flag{ &Float64Flag{ Name: "dest", @@ -1215,14 +1310,13 @@ func TestParseDestinationFloat64(t *testing.T) { } return nil }, - } - a.Run([]string{"run", "--dest", "10.2"}) + }).Run([]string{"run", "--dest", "10.2"}) } func TestParseMultiFloat64FromEnv(t *testing.T) { - clearenv() - os.Setenv("APP_TIMEOUT_SECONDS", "15.5") - a := App{ + os.Clearenv() + _ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5") + _ = (&App{ Flags: []Flag{ &Float64Flag{Name: "timeout", Aliases: []string{"t"}, EnvVars: []string{"APP_TIMEOUT_SECONDS"}}, }, @@ -1235,14 +1329,13 @@ func TestParseMultiFloat64FromEnv(t *testing.T) { } return nil }, - } - a.Run([]string{"run"}) + }).Run([]string{"run"}) } func TestParseMultiFloat64FromEnvCascade(t *testing.T) { - clearenv() - os.Setenv("APP_TIMEOUT_SECONDS", "15.5") - a := App{ + os.Clearenv() + _ = os.Setenv("APP_TIMEOUT_SECONDS", "15.5") + _ = (&App{ Flags: []Flag{ &Float64Flag{Name: "timeout", Aliases: []string{"t"}, EnvVars: []string{"COMPAT_TIMEOUT_SECONDS", "APP_TIMEOUT_SECONDS"}}, }, @@ -1255,15 +1348,14 @@ func TestParseMultiFloat64FromEnvCascade(t *testing.T) { } return nil }, - } - a.Run([]string{"run"}) + }).Run([]string{"run"}) } func TestParseMultiFloat64SliceFromEnv(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "0.1,-10.5") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "0.1,-10.5") - (&App{ + _ = (&App{ Flags: []Flag{ &Float64SliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewFloat64Slice(), EnvVars: []string{"APP_INTERVALS"}}, }, @@ -1280,10 +1372,10 @@ func TestParseMultiFloat64SliceFromEnv(t *testing.T) { } func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) { - clearenv() - os.Setenv("APP_INTERVALS", "0.1234,-10.5") + os.Clearenv() + _ = os.Setenv("APP_INTERVALS", "0.1234,-10.5") - (&App{ + _ = (&App{ Flags: []Flag{ &Float64SliceFlag{Name: "intervals", Aliases: []string{"i"}, Value: NewFloat64Slice(), EnvVars: []string{"COMPAT_INTERVALS", "APP_INTERVALS"}}, }, @@ -1300,7 +1392,7 @@ func TestParseMultiFloat64SliceFromEnvCascade(t *testing.T) { } func TestParseMultiBool(t *testing.T) { - a := App{ + _ = (&App{ Flags: []Flag{ &BoolFlag{Name: "serve", Aliases: []string{"s"}}, }, @@ -1313,13 +1405,36 @@ func TestParseMultiBool(t *testing.T) { } return nil }, - } - a.Run([]string{"run", "--serve"}) + }).Run([]string{"run", "--serve"}) +} + +func TestParseBoolShortOptionHandle(t *testing.T) { + _ = (&App{ + Commands: []*Command{ + { + Name: "foobar", + UseShortOptionHandling: true, + Action: func(ctx *Context) error { + if ctx.Bool("serve") != true { + t.Errorf("main name not set") + } + if ctx.Bool("option") != true { + t.Errorf("short name not set") + } + return nil + }, + Flags: []Flag{ + BoolFlag{Name: "serve, s"}, + BoolFlag{Name: "option, o"}, + }, + }, + }, + }).Run([]string{"run", "foobar", "-so"}) } func TestParseDestinationBool(t *testing.T) { var dest bool - a := App{ + _ = (&App{ Flags: []Flag{ &BoolFlag{ Name: "dest", @@ -1332,14 +1447,13 @@ func TestParseDestinationBool(t *testing.T) { } return nil }, - } - a.Run([]string{"run", "--dest"}) + }).Run([]string{"run", "--dest"}) } func TestParseMultiBoolFromEnv(t *testing.T) { - clearenv() - os.Setenv("APP_DEBUG", "1") - a := App{ + os.Clearenv() + _ = os.Setenv("APP_DEBUG", "1") + _ = (&App{ Flags: []Flag{ &BoolFlag{Name: "debug", Aliases: []string{"d"}, EnvVars: []string{"APP_DEBUG"}}, }, @@ -1352,14 +1466,13 @@ func TestParseMultiBoolFromEnv(t *testing.T) { } return nil }, - } - a.Run([]string{"run"}) + }).Run([]string{"run"}) } func TestParseMultiBoolFromEnvCascade(t *testing.T) { - clearenv() + os.Clearenv() os.Setenv("APP_DEBUG", "1") - a := App{ + _ = (&App{ Flags: []Flag{ &BoolFlag{Name: "debug", Aliases: []string{"d"}, EnvVars: []string{"COMPAT_DEBUG", "APP_DEBUG"}}, }, @@ -1372,97 +1485,93 @@ func TestParseMultiBoolFromEnvCascade(t *testing.T) { } return nil }, - } - a.Run([]string{"run"}) + }).Run([]string{"run"}) } -func TestParseMultiBoolTrue(t *testing.T) { - a := App{ - Flags: []Flag{ - &BoolFlag{Name: "implode", Aliases: []string{"i"}, Value: true}, - }, - Action: func(ctx *Context) error { - if ctx.Bool("implode") { - t.Errorf("main name not set") - } - if ctx.Bool("i") { - t.Errorf("short name not set") - } - return nil - }, +func TestParseBoolTFromEnv(t *testing.T) { + var boolTFlagTests = []struct { + input string + output bool + }{ + {"", false}, + {"1", true}, + {"false", false}, + {"true", true}, } - a.Run([]string{"run", "--implode=false"}) -} -func TestParseDestinationBoolTrue(t *testing.T) { - dest := true - - a := App{ - Flags: []Flag{ - &BoolFlag{ - Name: "dest", - Value: true, - Destination: &dest, + for _, test := range boolTFlagTests { + os.Clearenv() + _ = os.Setenv("DEBUG", test.input) + _ = (&App{ + Flags: []Flag{ + BoolTFlag{Name: "debug, d", EnvVar: "DEBUG"}, }, - }, - Action: func(ctx *Context) error { - if dest { - t.Errorf("expected destination Bool false") - } - return nil - }, + Action: func(ctx *Context) error { + if ctx.Bool("debug") != test.output { + t.Errorf("expected %+v to be parsed as %+v, instead was %+v", test.input, test.output, ctx.Bool("debug")) + } + if ctx.Bool("d") != test.output { + t.Errorf("expected %+v to be parsed as %+v, instead was %+v", test.input, test.output, ctx.Bool("d")) + } + return nil + }, + }).Run([]string{"run"}) } - a.Run([]string{"run", "--dest=false"}) } -func TestParseMultiBoolTrueFromEnv(t *testing.T) { - clearenv() - os.Setenv("APP_DEBUG", "0") - a := App{ - Flags: []Flag{ - &BoolFlag{ - Name: "debug", - Aliases: []string{"d"}, - Value: true, - EnvVars: []string{"APP_DEBUG"}, +func TestParseMultiBoolT(t *testing.T) { + _ = (&App{ + Flags: []Flag{ + &BoolFlag{Name: "implode", Aliases: []string{"i"}, Value: true}, + }, + Action: func(ctx *Context) error { + if ctx.Bool("implode") { + t.Errorf("main name not set") + } + if ctx.Bool("i") { + t.Errorf("short name not set") + } + return nil }, + }).Run([]string{"run", "--implode=false"}) +} + +func TestParseMultiBoolTFromEnv(t *testing.T) { + os.Clearenv() + _ = os.Setenv("APP_DEBUG", "0") + _ = (&App{ + Flags: []Flag{ + &BoolTFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, }, Action: func(ctx *Context) error { - if ctx.Bool("debug") { + if ctx.BoolT("debug") != false { t.Errorf("main name not set from env") } - if ctx.Bool("d") { + if ctx.BoolT("d") != false { t.Errorf("short name not set from env") } return nil }, - } - a.Run([]string{"run"}) + }).Run([]string{"run"}) } -func TestParseMultiBoolTrueFromEnvCascade(t *testing.T) { - clearenv() - os.Setenv("APP_DEBUG", "0") - a := App{ +func TestParseMultiBoolTFromEnvCascade(t *testing.T) { + os.Clearenv() + _ = os.Setenv("APP_DEBUG", "0") + _ = (&App{ Flags: []Flag{ - &BoolFlag{ - Name: "debug", - Aliases: []string{"d"}, - Value: true, - EnvVars: []string{"COMPAT_DEBUG", "APP_DEBUG"}, - }, + &BoolTFlag{Name: "debug, d", EnvVar: "COMPAT_DEBUG,APP_DEBUG"}, }, Action: func(ctx *Context) error { - if ctx.Bool("debug") { + if ctx.BoolT("debug") != false { t.Errorf("main name not set from env") } - if ctx.Bool("d") { + if ctx.BoolT("d") != false { t.Errorf("short name not set from env") } return nil }, - } - a.Run([]string{"run"}) + }).Run([]string{"run"}) } type Parser [2]string @@ -1488,7 +1597,7 @@ func (p *Parser) Get() interface{} { } func TestParseGeneric(t *testing.T) { - a := App{ + _ = (&App{ Flags: []Flag{ &GenericFlag{Name: "serve", Aliases: []string{"s"}, Value: &Parser{}}, }, @@ -1501,14 +1610,14 @@ func TestParseGeneric(t *testing.T) { } return nil }, - } - a.Run([]string{"run", "-s", "10,20"}) + }).Run([]string{"run", "-s", "10,20"}) } func TestParseGenericFromEnv(t *testing.T) { - clearenv() - os.Setenv("APP_SERVE", "20,30") - a := App{ + os.Clearenv() + _ = os.Setenv("APP_SERVE", "20,30") + _ = (&App{ + Flags: []Flag{ &GenericFlag{ Name: "serve", @@ -1526,14 +1635,13 @@ func TestParseGenericFromEnv(t *testing.T) { } return nil }, - } - a.Run([]string{"run"}) + }).Run([]string{"run"}) } func TestParseGenericFromEnvCascade(t *testing.T) { - clearenv() - os.Setenv("APP_FOO", "99,2000") - a := App{ + os.Clearenv() + _ = os.Setenv("APP_FOO", "99,2000") + _ = (&App{ Flags: []Flag{ &GenericFlag{ Name: "foos", @@ -1547,13 +1655,47 @@ func TestParseGenericFromEnvCascade(t *testing.T) { } return nil }, + }).Run([]string{"run"}) +} + +func TestFlagFromFile(t *testing.T) { + os.Clearenv() + os.Setenv("APP_FOO", "123") + + temp, err := ioutil.TempFile("", "urfave_cli_test") + if err != nil { + t.Error(err) + return + } + _, _ = io.WriteString(temp, "abc") + _ = temp.Close() + defer func() { + _ = os.Remove(temp.Name()) + }() + + var filePathTests = []struct { + path string + name string + expected string + }{ + {"file-does-not-exist", "APP_BAR", ""}, + {"file-does-not-exist", "APP_FOO", "123"}, + {"file-does-not-exist", "APP_FOO,APP_BAR", "123"}, + {temp.Name(), "APP_FOO", "123"}, + {temp.Name(), "APP_BAR", "abc"}, + } + + for _, filePathTest := range filePathTests { + got, _ := flagFromEnvOrFile(filePathTest.name, filePathTest.path) + if want := filePathTest.expected; got != want { + t.Errorf("Did not expect %v - Want %v", got, want) + } } - a.Run([]string{"run"}) } func TestStringSlice_Serialized_Set(t *testing.T) { sl0 := NewStringSlice("a", "b") - ser0 := sl0.Serialized() + ser0 := sl0.Serialize() if len(ser0) < len(slPfx) { t.Fatalf("serialized shorter than expected: %q", ser0) @@ -1569,7 +1711,7 @@ func TestStringSlice_Serialized_Set(t *testing.T) { func TestIntSlice_Serialized_Set(t *testing.T) { sl0 := NewIntSlice(1, 2) - ser0 := sl0.Serialized() + ser0 := sl0.Serialize() if len(ser0) < len(slPfx) { t.Fatalf("serialized shorter than expected: %q", ser0) @@ -1585,7 +1727,7 @@ func TestIntSlice_Serialized_Set(t *testing.T) { func TestInt64Slice_Serialized_Set(t *testing.T) { sl0 := NewInt64Slice(int64(1), int64(2)) - ser0 := sl0.Serialized() + ser0 := sl0.Serialize() if len(ser0) < len(slPfx) { t.Fatalf("serialized shorter than expected: %q", ser0) diff --git a/flag_uint.go b/flag_uint.go new file mode 100644 index 0000000..7a3e943 --- /dev/null +++ b/flag_uint.go @@ -0,0 +1,104 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// UintFlag is a flag with type uint +type UintFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value uint + DefaultText string + Destination *uint +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *UintFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *UintFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *UintFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *UintFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *UintFlag) GetUsage() string { + return f.Usage +} + +// Apply populates the flag given the flag set and environment +func (f *UintFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valInt, err := strconv.ParseUint(val, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as uint value for flag %s: %s", val, f.Name, err) + } + + f.Value = uint(valInt) + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.UintVar(f.Destination, name, f.Value, f.Usage) + continue + } + set.Uint(name, f.Value, f.Usage) + } + + return nil +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *UintFlag) GetValue() string { + return fmt.Sprintf("%d", f.Value) +} + +// Uint looks up the value of a local UintFlag, returns +// 0 if not found +func (c *Context) Uint(name string) uint { + return lookupUint(name, c.flagSet) +} + +// GlobalUint looks up the value of a global UintFlag, returns +// 0 if not found +//func (c *Context) GlobalUint(name string) uint { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupUint(name, fs) +// } +// return 0 +//} + +func lookupUint(name string, set *flag.FlagSet) uint { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return uint(parsed) + } + return 0 +} diff --git a/flag_uint64.go b/flag_uint64.go new file mode 100644 index 0000000..65534c7 --- /dev/null +++ b/flag_uint64.go @@ -0,0 +1,104 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// Uint64Flag is a flag with type uint64 +type Uint64Flag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value uint64 + DefaultText string + Destination *uint64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Uint64Flag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *Uint64Flag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *Uint64Flag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *Uint64Flag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *Uint64Flag) GetUsage() string { + return f.Usage +} + +// Apply populates the flag given the flag set and environment +func (f *Uint64Flag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valInt, err := strconv.ParseUint(val, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", val, f.Name, err) + } + + f.Value = valInt + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.Uint64Var(f.Destination, name, f.Value, f.Usage) + continue + } + set.Uint64(name, f.Value, f.Usage) + } + + return nil +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Uint64Flag) GetValue() string { + return fmt.Sprintf("%d", f.Value) +} + +// Uint64 looks up the value of a local Uint64Flag, returns +// 0 if not found +func (c *Context) Uint64(name string) uint64 { + return lookupUint64(name, c.flagSet) +} + +// GlobalUint64 looks up the value of a global Uint64Flag, returns +// 0 if not found +//func (c *Context) GlobalUint64(name string) uint64 { +// if fs := lookupGlobalFlagSet(name, c); fs != nil { +// return lookupUint64(name, fs) +// } +// return 0 +//} + +func lookupUint64(name string, set *flag.FlagSet) uint64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} diff --git a/funcs.go b/funcs.go index fa7d502..9ffe246 100644 --- a/funcs.go +++ b/funcs.go @@ -1,7 +1,7 @@ package cli -// ShellCompleteFunc is an action to execute when the shell completion flag is set -type ShellCompleteFunc func(*Context) +// BashCompleteFunc is an action to execute when the shell completion flag is set +type BashCompleteFunc func(*Context) // BeforeFunc is an action to execute before any subcommands are run, but after // the context is ready if a non-nil error is returned, no subcommands are run @@ -23,6 +23,22 @@ type CommandNotFoundFunc func(*Context, string) // is displayed and the execution is interrupted. type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error +// ExitErrHandlerFunc is executed if provided in order to handle ExitError values +// returned by Actions and Before/After functions. +type ExitErrHandlerFunc func(context *Context, err error) + // FlagStringFunc is used by the help generation to display a flag, which is // expected to be a single line. type FlagStringFunc func(Flag) string + +// FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix +// text for a flag's full name. +type FlagNamePrefixFunc func(fullName []string, placeholder string) string + +// FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help +// with the environment variable details. +type FlagEnvHintFunc func(envVars []string, str string) string + +// FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help +// with the file path details. +type FlagFileHintFunc func(filePath, str string) string diff --git a/generate-flag-types b/generate-flag-types deleted file mode 100755 index d77d470..0000000 --- a/generate-flag-types +++ /dev/null @@ -1,255 +0,0 @@ -#!/usr/bin/env python -""" -The flag types that ship with the cli library have many things in common, and -so we can take advantage of the `go generate` command to create much of the -source code from a list of definitions. These definitions attempt to cover -the parts that vary between flag types, and should evolve as needed. - -An example of the minimum definition needed is: - - { - "name": "SomeType", - "type": "sometype", - "context_default": "nil" - } - -In this example, the code generated for the `cli` package will include a type -named `SomeTypeFlag` that is expected to wrap a value of type `sometype`. -Fetching values by name via `*cli.Context` will default to a value of `nil`. - -A more complete, albeit somewhat redundant, example showing all available -definition keys is: - - { - "name": "VeryMuchType", - "type": "*VeryMuchType", - "value": true, - "dest": false, - "doctail": " which really only wraps a []float64, oh well!", - "context_type": "[]float64", - "context_default": "nil", - "parser": "parseVeryMuchType(f.Value.String())", - "parser_cast": "[]float64(parsed)" - } - -The meaning of each field is as follows: - - name (string) - The type "name", which will be suffixed with - `Flag` when generating the type definition - for `cli` and the wrapper type for `altsrc` - type (string) - The type that the generated `Flag` type for - `cli` is expected to "contain" as its `.Value` - member - value (bool) - Should the generated `cli` type have a `Value` - member? - dest (bool) - Should the generated `cli` type support a - destination pointer? - doctail (string) - Additional docs for the `cli` flag type comment - context_type (string) - The literal type used in the `*cli.Context` - reader func signature - context_default (string) - The literal value used as the default by the - `*cli.Context` reader funcs when no value is - present - parser (string) - Literal code used to parse the flag `f`, - expected to have a return signature of - (value, error) - parser_cast (string) - Literal code used to cast the `parsed` value - returned from the `parser` code -""" - -from __future__ import print_function, unicode_literals - -import argparse -import json -import os -import subprocess -import sys -import tempfile -import textwrap - - -_PY3 = sys.version_info.major == 3 - - -class _FancyFormatter(argparse.ArgumentDefaultsHelpFormatter, - argparse.RawDescriptionHelpFormatter): - pass - - -def main(sysargs=sys.argv[:]): - parser = argparse.ArgumentParser( - description='Generate flag type code!', - formatter_class=_FancyFormatter) - parser.add_argument( - 'package', - type=str, default='cli', choices=_WRITEFUNCS.keys(), - help='Package for which flag types will be generated' - ) - parser.add_argument( - '-i', '--in-json', - type=argparse.FileType('r'), - default=sys.stdin, - help='Input JSON file which defines each type to be generated' - ) - parser.add_argument( - '-o', '--out-go', - type=argparse.FileType('w'), - default=sys.stdout, - help='Output file/stream to which generated source will be written' - ) - parser.epilog = __doc__ - - args = parser.parse_args(sysargs[1:]) - _generate_flag_types(_WRITEFUNCS[args.package], args.out_go, args.in_json) - return 0 - - -def _generate_flag_types(writefunc, output_go, input_json): - types = json.load(input_json) - - tmp = _get_named_tmp_go() - writefunc(tmp, types) - tmp.close() - - new_content = subprocess.check_output( - ['goimports', tmp.name] - ).decode('utf-8') - - print(new_content, file=output_go, end='') - output_go.flush() - os.remove(tmp.name) - - -def _get_named_tmp_go(): - tmp_args = dict(suffix='.go', mode='w', delete=False) - if _PY3: - tmp_args['encoding'] = 'utf-8' - return tempfile.NamedTemporaryFile(**tmp_args) - - -def _set_typedef_defaults(typedef): - typedef.setdefault('doctail', '') - typedef.setdefault('context_type', typedef['type']) - typedef.setdefault('dest', True) - typedef.setdefault('parser', 'f.Value, error(nil)') - typedef.setdefault('parser_cast', 'parsed') - - -def _write_cli_flag_types(outfile, types): - _fwrite(outfile, """\ - package cli - - // WARNING: This file is generated! - - """) - - for typedef in types: - _set_typedef_defaults(typedef) - - _fwrite(outfile, """\ - // {name}Flag is a flag with type {type}{doctail} - type {name}Flag struct {{ - Name string - Aliases []string - Usage string - EnvVars []string - Hidden bool - Value {type} - DefaultText string - """.format(**typedef)) - - if typedef['dest']: - _fwrite(outfile, """\ - Destination *{type} - """.format(**typedef)) - - _fwrite(outfile, "\n}\n\n") - - _fwrite(outfile, """\ - // String returns a readable representation of this value - // (for usage defaults) - func (f *{name}Flag) String() string {{ - return FlagStringer(f) - }} - - // Names returns the names of the flag - func (f *{name}Flag) Names() []string {{ - return flagNames(f) - }} - - // {name} looks up the value of a local {name}Flag, returns - // {context_default} if not found - func (c *Context) {name}(name string) {context_type} {{ - if fs := lookupFlagSet(name, c); fs != nil {{ - return lookup{name}(name, fs) - }} - return {context_default} - }} - - func lookup{name}(name string, set *flag.FlagSet) {context_type} {{ - f := set.Lookup(name) - if f != nil {{ - parsed, err := {parser} - if err != nil {{ - return {context_default} - }} - return {parser_cast} - }} - return {context_default} - }} - """.format(**typedef)) - - -def _write_altsrc_flag_types(outfile, types): - _fwrite(outfile, """\ - package altsrc - - import "gopkg.in/urfave/cli.v2" - - // WARNING: This file is generated! - - """) - - for typedef in types: - _set_typedef_defaults(typedef) - - _fwrite(outfile, """\ - // {name}Flag is the flag type that wraps cli.{name}Flag to allow - // for other values to be specified - type {name}Flag struct {{ - *cli.{name}Flag - set *flag.FlagSet - }} - - // New{name}Flag creates a new {name}Flag - func New{name}Flag(fl *cli.{name}Flag) *{name}Flag {{ - return &{name}Flag{{{name}Flag: fl, set: nil}} - }} - - // Apply saves the flagSet for later usage calls, then calls the - // wrapped {name}Flag.Apply - func (f *{name}Flag) Apply(set *flag.FlagSet) {{ - f.set = set - f.{name}Flag.Apply(set) - }} - - // ApplyWithError saves the flagSet for later usage calls, then calls the - // wrapped {name}Flag.ApplyWithError - func (f *{name}Flag) ApplyWithError(set *flag.FlagSet) error {{ - f.set = set - return f.{name}Flag.ApplyWithError(set) - }} - """.format(**typedef)) - - -def _fwrite(outfile, text): - print(textwrap.dedent(text), end=None, file=outfile) - - -_WRITEFUNCS = { - 'cli': _write_cli_flag_types, - 'altsrc': _write_altsrc_flag_types -} - -if __name__ == '__main__': - sys.exit(main()) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c38d41c --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/urfave/cli/v2 + +go 1.11 + +require ( + github.com/BurntSushi/toml v0.3.1 + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ef121ff --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/help.go b/help.go index 02fcf33..155654f 100644 --- a/help.go +++ b/help.go @@ -7,6 +7,7 @@ import ( "strings" "text/tabwriter" "text/template" + "unicode/utf8" ) // AppHelpTemplate is the text template for the Default help topic. @@ -89,7 +90,7 @@ var helpCommand = &Command{ return ShowCommandHelp(c, args.First()) } - ShowAppHelp(c) + _ = ShowAppHelp(c) return nil }, } @@ -129,7 +130,7 @@ var VersionPrinter = printVersion // ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. func ShowAppHelpAndExit(c *Context, exitCode int) { - ShowAppHelp(c) + _ = ShowAppHelp(c) os.Exit(exitCode) } @@ -153,19 +154,94 @@ func ShowAppHelp(c *Context) (err error) { // DefaultAppComplete prints the list of subcommands as the default app completion method func DefaultAppComplete(c *Context) { - for _, command := range c.App.Commands { + DefaultCompleteWithFlags(nil)(c) +} + +func printCommandSuggestions(commands []Command, writer io.Writer) { + for _, command := range commands { if command.Hidden { continue } - for _, name := range command.Names() { - fmt.Fprintln(c.App.Writer, name) + if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" { + for _, name := range command.Names() { + _, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage) + } + } else { + for _, name := range command.Names() { + _, _ = fmt.Fprintf(writer, "%s\n", name) + } + } + } +} + +func cliArgContains(flagName string) bool { + for _, name := range strings.Split(flagName, ",") { + name = strings.TrimSpace(name) + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 + } + flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + for _, a := range os.Args { + if a == flag { + return true + } + } + } + return false +} + +func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { + cur := strings.TrimPrefix(lastArg, "-") + cur = strings.TrimPrefix(cur, "-") + for _, flag := range flags { + if bflag, ok := flag.(BoolFlag); ok && bflag.Hidden { + continue + } + for _, name := range strings.Split(flag.GetName(), ",") { + name = strings.TrimSpace(name) + // this will get total count utf8 letters in flag name + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 // resuse this count to generate single - or -- in flag completion + } + // if flag name has more than one utf8 letter and last argument in cli has -- prefix then + // skip flag completion for short flags example -v or -x + if strings.HasPrefix(lastArg, "--") && count == 1 { + continue + } + // match if last argument matches this flag and it is not repeated + if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(flag.GetName()) { + flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + _, _ = fmt.Fprintln(writer, flagCompletion) + } + } + } +} + +func DefaultCompleteWithFlags(cmd *Command) func(c *Context) { + return func(c *Context) { + if len(os.Args) > 2 { + lastArg := os.Args[len(os.Args)-2] + if strings.HasPrefix(lastArg, "-") { + printFlagSuggestions(lastArg, c.App.Flags, c.App.Writer) + if cmd != nil { + printFlagSuggestions(lastArg, cmd.Flags, c.App.Writer) + } + return + } + } + if cmd != nil { + printCommandSuggestions(cmd.Subcommands, c.App.Writer) + } else { + printCommandSuggestions(c.App.Commands, c.App.Writer) } } } // ShowCommandHelpAndExit - exits with code after showing help func ShowCommandHelpAndExit(c *Context, command string, code int) { - ShowCommandHelp(c, command) + _ = ShowCommandHelp(c, command) os.Exit(code) } @@ -215,49 +291,57 @@ func ShowVersion(c *Context) { } func printVersion(c *Context) { - fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) + _, _ = fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) } // ShowCompletions prints the lists of commands within a given context func ShowCompletions(c *Context) { a := c.App - if a != nil && a.ShellComplete != nil { - a.ShellComplete(c) + if a != nil && a.BashComplete != nil { + a.BashComplete(c) } } // ShowCommandCompletions prints the custom completions for a given command func ShowCommandCompletions(ctx *Context, command string) { c := ctx.App.Command(command) + //TODO: Resolve +//<<<<<<< HEAD if c != nil && c.ShellComplete != nil { c.ShellComplete(ctx) +//======= +// if c != nil { +// if c.BashComplete != nil { +// c.BashComplete(ctx) +// } else { +// DefaultCompleteWithFlags(c)(ctx) +// } +//>>>>>>> master } + } func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) { funcMap := template.FuncMap{ "join": strings.Join, } - if customFunc != nil { - for key, value := range customFunc { - funcMap[key] = value - } + for key, value := range customFunc { + funcMap[key] = value } w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) - errDebug := os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" - err := t.Execute(w, data) if err != nil { - if errDebug { - fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err) + // If the writer is closed, t.Execute will fail, and there's nothing + // we can do to recover. + if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" { + _, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err) } return } - - w.Flush() + _ = w.Flush() } func printHelp(out io.Writer, templ string, data interface{}) { @@ -286,7 +370,7 @@ func checkHelp(c *Context) bool { func checkCommandHelp(c *Context, name string) bool { if c.Bool("h") || c.Bool("help") { - ShowCommandHelp(c, name) + _ = ShowCommandHelp(c, name) return true } @@ -295,7 +379,7 @@ func checkCommandHelp(c *Context, name string) bool { func checkSubcommandHelp(c *Context) bool { if c.Bool("h") || c.Bool("help") { - ShowSubcommandHelp(c) + _ = ShowSubcommandHelp(c) return true } diff --git a/help_test.go b/help_test.go index 375b28e..6d3a6a9 100644 --- a/help_test.go +++ b/help_test.go @@ -15,9 +15,9 @@ func Test_ShowAppHelp_NoAuthor(t *testing.T) { c := NewContext(app, nil, nil) - ShowAppHelp(c) + _ = ShowAppHelp(c) - if bytes.Index(output.Bytes(), []byte("AUTHOR(S):")) != -1 { + if bytes.Contains(output.Bytes(), []byte("AUTHOR(S):")) { t.Errorf("expected\n%snot to include %s", output.String(), "AUTHOR(S):") } } @@ -30,9 +30,9 @@ func Test_ShowAppHelp_NoVersion(t *testing.T) { c := NewContext(app, nil, nil) - ShowAppHelp(c) + _ = ShowAppHelp(c) - if bytes.Index(output.Bytes(), []byte("VERSION:")) != -1 { + if bytes.Contains(output.Bytes(), []byte("VERSION:")) { t.Errorf("expected\n%snot to include %s", output.String(), "VERSION:") } } @@ -45,9 +45,9 @@ func Test_ShowAppHelp_HideVersion(t *testing.T) { c := NewContext(app, nil, nil) - ShowAppHelp(c) + _ = ShowAppHelp(c) - if bytes.Index(output.Bytes(), []byte("VERSION:")) != -1 { + if bytes.Contains(output.Bytes(), []byte("VERSION:")) { t.Errorf("expected\n%snot to include %s", output.String(), "VERSION:") } } @@ -77,7 +77,7 @@ func Test_Help_Custom_Flags(t *testing.T) { } output := new(bytes.Buffer) app.Writer = output - app.Run([]string{"test", "-h"}) + _ = app.Run([]string{"test", "-h"}) if output.Len() > 0 { t.Errorf("unexpected output: %s", output.String()) } @@ -108,7 +108,7 @@ func Test_Version_Custom_Flags(t *testing.T) { } output := new(bytes.Buffer) app.Writer = output - app.Run([]string{"test", "-v"}) + _ = app.Run([]string{"test", "-v"}) if output.Len() > 0 { t.Errorf("unexpected output: %s", output.String()) } @@ -118,7 +118,7 @@ func Test_helpCommand_Action_ErrorIfNoTopic(t *testing.T) { app := &App{} set := flag.NewFlagSet("test", 0) - set.Parse([]string{"foo"}) + _ = set.Parse([]string{"foo"}) c := NewContext(app, set, nil) @@ -128,9 +128,9 @@ func Test_helpCommand_Action_ErrorIfNoTopic(t *testing.T) { t.Fatalf("expected error from helpCommand.Action(), but got nil") } - exitErr, ok := err.(*exitError) + exitErr, ok := err.(*ExitError) if !ok { - t.Fatalf("expected *exitError from helpCommand.Action(), but instead got: %v", err.Error()) + t.Fatalf("expected *ExitError from helpCommand.Action(), but instead got: %v", err.Error()) } if !strings.HasPrefix(exitErr.Error(), "No help topic for") { @@ -146,7 +146,7 @@ func Test_helpCommand_InHelpOutput(t *testing.T) { app := &App{} output := &bytes.Buffer{} app.Writer = output - app.Run([]string{"test", "--help"}) + _ = app.Run([]string{"test", "--help"}) s := output.String() @@ -163,7 +163,7 @@ func Test_helpSubcommand_Action_ErrorIfNoTopic(t *testing.T) { app := &App{} set := flag.NewFlagSet("test", 0) - set.Parse([]string{"foo"}) + _ = set.Parse([]string{"foo"}) c := NewContext(app, set, nil) @@ -173,9 +173,9 @@ func Test_helpSubcommand_Action_ErrorIfNoTopic(t *testing.T) { t.Fatalf("expected error from helpCommand.Action(), but got nil") } - exitErr, ok := err.(*exitError) + exitErr, ok := err.(*ExitError) if !ok { - t.Fatalf("expected *exitError from helpCommand.Action(), but instead got: %v", err.Error()) + t.Fatalf("expected *ExitError from helpCommand.Action(), but instead got: %v", err.Error()) } if !strings.HasPrefix(exitErr.Error(), "No help topic for") { @@ -202,7 +202,7 @@ func TestShowAppHelp_CommandAliases(t *testing.T) { output := &bytes.Buffer{} app.Writer = output - app.Run([]string{"foo", "--help"}) + _ = app.Run([]string{"foo", "--help"}) if !strings.Contains(output.String(), "frobbly, fr, frob") { t.Errorf("expected output to include all command aliases; got: %q", output.String()) @@ -224,7 +224,7 @@ func TestShowCommandHelp_CommandAliases(t *testing.T) { output := &bytes.Buffer{} app.Writer = output - app.Run([]string{"foo", "help", "fr"}) + _ = app.Run([]string{"foo", "help", "fr"}) if !strings.Contains(output.String(), "frobbly") { t.Errorf("expected output to include command name; got: %q", output.String()) @@ -250,7 +250,7 @@ func TestShowSubcommandHelp_CommandAliases(t *testing.T) { output := &bytes.Buffer{} app.Writer = output - app.Run([]string{"foo", "help"}) + _ = app.Run([]string{"foo", "help"}) if !strings.Contains(output.String(), "frobbly, fr, frob, bork") { t.Errorf("expected output to include all command aliases; got: %q", output.String()) @@ -284,7 +284,7 @@ EXAMPLES: } output := &bytes.Buffer{} app.Writer = output - app.Run([]string{"foo", "help", "frobbly"}) + _ = app.Run([]string{"foo", "help", "frobbly"}) if strings.Contains(output.String(), "2. Frobbly runs without this param locally.") { t.Errorf("expected output to exclude \"2. Frobbly runs without this param locally.\"; got: %q", output.String()) @@ -312,7 +312,7 @@ func TestShowSubcommandHelp_CommandUsageText(t *testing.T) { output := &bytes.Buffer{} app.Writer = output - app.Run([]string{"foo", "frobbly", "--help"}) + _ = app.Run([]string{"foo", "frobbly", "--help"}) if !strings.Contains(output.String(), "this is usage text") { t.Errorf("expected output to include usage text; got: %q", output.String()) @@ -336,7 +336,7 @@ func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) { output := &bytes.Buffer{} app.Writer = output - app.Run([]string{"foo", "frobbly", "bobbly", "--help"}) + _ = app.Run([]string{"foo", "frobbly", "bobbly", "--help"}) if !strings.Contains(output.String(), "this is usage text") { t.Errorf("expected output to include usage text; got: %q", output.String()) @@ -364,7 +364,7 @@ func TestShowAppHelp_HiddenCommand(t *testing.T) { output := &bytes.Buffer{} app.Writer = output - app.Run([]string{"app", "--help"}) + _ = app.Run([]string{"app", "--help"}) if strings.Contains(output.String(), "secretfrob") { t.Errorf("expected output to exclude \"secretfrob\"; got: %q", output.String()) @@ -422,7 +422,7 @@ VERSION: output := &bytes.Buffer{} app.Writer = output - app.Run([]string{"app", "--help"}) + _ = app.Run([]string{"app", "--help"}) if strings.Contains(output.String(), "secretfrob") { t.Errorf("expected output to exclude \"secretfrob\"; got: %q", output.String()) diff --git a/helpers_unix_test.go b/helpers_unix_test.go deleted file mode 100644 index ae27fc5..0000000 --- a/helpers_unix_test.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build darwin dragonfly freebsd linux netbsd openbsd solaris - -package cli - -import "os" - -func clearenv() { - os.Clearenv() -} diff --git a/helpers_windows_test.go b/helpers_windows_test.go deleted file mode 100644 index 4eb84f9..0000000 --- a/helpers_windows_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package cli - -import ( - "os" - "syscall" -) - -// os.Clearenv() doesn't actually unset variables on Windows -// See: https://github.com/golang/go/issues/17902 -func clearenv() { - for _, s := range os.Environ() { - for j := 1; j < len(s); j++ { - if s[j] == '=' { - keyp, _ := syscall.UTF16PtrFromString(s[0:j]) - syscall.SetEnvironmentVariable(keyp, nil) - break - } - } - } -} diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..865accf --- /dev/null +++ b/parse.go @@ -0,0 +1,80 @@ +package cli + +import ( + "flag" + "strings" +) + +type iterativeParser interface { + newFlagSet() (*flag.FlagSet, error) + useShortOptionHandling() bool +} + +// To enable short-option handling (e.g., "-it" vs "-i -t") we have to +// iteratively catch parsing errors. This way we achieve LR parsing without +// transforming any arguments. Otherwise, there is no way we can discriminate +// combined short options from common arguments that should be left untouched. +func parseIter(ip iterativeParser, args []string) (*flag.FlagSet, error) { + for { + set, err := ip.newFlagSet() + if err != nil { + return nil, err + } + + err = set.Parse(args) + if !ip.useShortOptionHandling() || err == nil { + return set, err + } + + errStr := err.Error() + trimmed := strings.TrimPrefix(errStr, "flag provided but not defined: ") + if errStr == trimmed { + return nil, err + } + + // regenerate the initial args with the split short opts + newArgs := []string{} + for i, arg := range args { + if arg != trimmed { + newArgs = append(newArgs, arg) + continue + } + + shortOpts := splitShortOptions(set, trimmed) + if len(shortOpts) == 1 { + return nil, err + } + + // add each short option and all remaining arguments + newArgs = append(newArgs, shortOpts...) + newArgs = append(newArgs, args[i+1:]...) + args = newArgs + } + } +} + +func splitShortOptions(set *flag.FlagSet, arg string) []string { + shortFlagsExist := func(s string) bool { + for _, c := range s[1:] { + if f := set.Lookup(string(c)); f == nil { + return false + } + } + return true + } + + if !isSplittable(arg) || !shortFlagsExist(arg) { + return []string{arg} + } + + separated := make([]string, 0, len(arg)-1) + for _, flagChar := range arg[1:] { + separated = append(separated, "-"+string(flagChar)) + } + + return separated +} + +func isSplittable(flagArg string) bool { + return strings.HasPrefix(flagArg, "-") && !strings.HasPrefix(flagArg, "--") && len(flagArg) > 2 +} diff --git a/runtests b/runtests deleted file mode 100755 index d2c5ea6..0000000 --- a/runtests +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function, unicode_literals - -import argparse -import codecs -import glob -import os -import platform -import shutil -import sys -import tempfile - -from subprocess import check_call, check_output - - -_PY3 = sys.version_info.major == 3 -_WINDOWS = platform.system().lower() == 'windows' -_PACKAGE_NAME = os.environ.get( - 'CLI_PACKAGE_NAME', 'github.com/urfave/cli' -) -_TARGETS = {} - - -def main(sysargs=sys.argv[:]): - parser = argparse.ArgumentParser() - parser.add_argument( - 'target', nargs='?', choices=tuple(_TARGETS.keys()), default='test' - ) - args = parser.parse_args(sysargs[1:]) - - _TARGETS[args.target]() - return 0 - - -def _target(func): - _TARGETS[func.__name__.strip('_')] = func - return func - - -@_target -def _test(): - if _go_version() < 'go1.2': - _run('go test -v .') - return - - coverprofiles = [] - for subpackage in ['', 'altsrc']: - coverprofile = 'cli.coverprofile' - if subpackage != '': - coverprofile = '{}.coverprofile'.format(subpackage) - - coverprofiles.append(coverprofile) - - _run('go test -v'.split() + [ - '-coverprofile={}'.format(coverprofile), - ('{}/{}'.format(_PACKAGE_NAME, subpackage)).rstrip('/') - ]) - - combined_name = _combine_coverprofiles(coverprofiles) - _run('go tool cover -func={}'.format(combined_name)) - os.remove(combined_name) - - -@_target -def _gfmrun(): - go_version = _go_version() - if go_version < 'go1.3': - print('runtests: skip on {}'.format(go_version), file=sys.stderr) - return - _run(['gfmrun', '-c', str(_gfmrun_count()), '-s', 'README.md']) - - -@_target -def _vet(): - _run('go vet ./...') - - -@_target -def _migrations(): - go_version = _go_version() - if go_version < 'go1.3': - print('runtests: skip on {}'.format(go_version), file=sys.stderr) - return - - migration_script = os.path.abspath( - os.environ.get('V1TOV2', './cli-v1-to-v2') - ) - v1_readme_url = os.environ.get( - 'V1README', - 'https://raw.githubusercontent.com/urfave/cli/v1/README.md' - ) - - tmpdir = tempfile.mkdtemp() - try: - os.chdir(tmpdir) - _run('curl -sSL -o README.md {}'.format(v1_readme_url).split()) - _run('gfmrun extract -o .'.split()) - - for gofile in glob.glob('*.go'): - for i in (0, 1): - _run(['python', migration_script, '-w', gofile]) - _run('go build -o tmp.out {}'.format(gofile).split()) - finally: - if os.environ.get('NOCLEAN', '') == '': - shutil.rmtree(tmpdir, ignore_errors=True) - - -@_target -def _toc(): - exe = ['bash'] if _WINDOWS else [] - _run(exe + [ - os.path.join('node_modules', '.bin', 'markdown-toc'), - '-i', 'README.md' - ]) - _run('git diff --exit-code') - - -@_target -def _gen(): - go_version = _go_version() - if go_version < 'go1.5': - print('runtests: skip on {}'.format(go_version), file=sys.stderr) - return - - _run('go generate ./...') - _run('git diff --exit-code') - - -def _run(command): - if hasattr(command, 'split'): - command = command.split() - print('runtests: {}'.format(' '.join(command)), file=sys.stderr) - sys.stderr.flush() - check_call(command) - - -def _gfmrun_count(): - with codecs.open('README.md', 'r', 'utf-8') as infile: - lines = infile.read().splitlines() - return len(list(filter(_is_go_runnable, lines))) - - -def _is_go_runnable(line): - return line.startswith('package main') - - -def _go_version(): - return check_output('go version'.split()).decode('utf-8').split()[2] - - -def _combine_coverprofiles(coverprofiles): - tmp_args = dict(suffix='.coverprofile', mode='w', delete=False) - if _PY3: - tmp_args['encoding'] = 'utf-8' - - combined = tempfile.NamedTemporaryFile(**tmp_args) - combined.write('mode: set\n') - - for coverprofile in coverprofiles: - with codecs.open(coverprofile, 'r', 'utf-8') as infile: - for line in infile.readlines(): - if not line.startswith('mode: '): - combined.write(line) - - combined.flush() - name = combined.name - combined.close() - return name - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/sort.go b/sort.go new file mode 100644 index 0000000..23d1c2f --- /dev/null +++ b/sort.go @@ -0,0 +1,29 @@ +package cli + +import "unicode" + +// lexicographicLess compares strings alphabetically considering case. +func lexicographicLess(i, j string) bool { + iRunes := []rune(i) + jRunes := []rune(j) + + lenShared := len(iRunes) + if lenShared > len(jRunes) { + lenShared = len(jRunes) + } + + for index := 0; index < lenShared; index++ { + ir := iRunes[index] + jr := jRunes[index] + + if lir, ljr := unicode.ToLower(ir), unicode.ToLower(jr); lir != ljr { + return lir < ljr + } + + if ir != jr { + return ir < jr + } + } + + return i < j +} diff --git a/sort_test.go b/sort_test.go new file mode 100644 index 0000000..662ef9b --- /dev/null +++ b/sort_test.go @@ -0,0 +1,30 @@ +package cli + +import "testing" + +var lexicographicLessTests = []struct { + i string + j string + expected bool +}{ + {"", "a", true}, + {"a", "", false}, + {"a", "a", false}, + {"a", "A", false}, + {"A", "a", true}, + {"aa", "a", false}, + {"a", "aa", true}, + {"a", "b", true}, + {"a", "B", true}, + {"A", "b", true}, + {"A", "B", true}, +} + +func TestLexicographicLess(t *testing.T) { + for _, test := range lexicographicLessTests { + actual := lexicographicLess(test.i, test.j) + if test.expected != actual { + t.Errorf(`expected string "%s" to come before "%s"`, test.i, test.j) + } + } +} diff --git a/template.go b/template.go new file mode 100644 index 0000000..c631fb9 --- /dev/null +++ b/template.go @@ -0,0 +1,121 @@ +package cli + +// AppHelpTemplate is the text template for the Default help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var AppHelpTemplate = `NAME: + {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} + +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + +VERSION: + {{.Version}}{{end}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if len .Authors}} + +AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: + {{range $index, $author := .Authors}}{{if $index}} + {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} + +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + + {{.Name}}:{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + +GLOBAL OPTIONS: + {{range $index, $option := .VisibleFlags}}{{if $index}} + {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} + +COPYRIGHT: + {{.Copyright}}{{end}} +` + +// CommandHelpTemplate is the text template for the command help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var CommandHelpTemplate = `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} + +CATEGORY: + {{.Category}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if .VisibleFlags}} + +OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +` + +// SubcommandHelpTemplate is the text template for the subcommand help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var SubcommandHelpTemplate = `NAME: + {{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}} + +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} + +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + + {{.Name}}:{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + +OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +` + +var MarkdownDocTemplate = `% {{ .App.Name }}(8) {{ .App.Description }} + +% {{ .App.Author }} + +# NAME + +{{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }} + +# SYNOPSIS + +{{ .App.Name }} +{{ if .SynopsisArgs }} +` + "```" + ` +{{ range $v := .SynopsisArgs }}{{ $v }}{{ end }}` + "```" + ` +{{ end }}{{ if .App.UsageText }} +# DESCRIPTION + +{{ .App.UsageText }} +{{ end }} +**Usage**: + +` + "```" + ` +{{ .App.Name }} [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +` + "```" + ` +{{ if .GlobalArgs }} +# GLOBAL OPTIONS +{{ range $v := .GlobalArgs }} +{{ $v }}{{ end }} +{{ end }}{{ if .Commands }} +# COMMANDS +{{ range $v := .Commands }} +{{ $v }}{{ end }}{{ end }}` + +var FishCompletionTemplate = `# {{ .App.Name }} fish shell completion + +function __fish_{{ .App.Name }}_no_subcommand --description 'Test if there has been any subcommand yet' + for i in (commandline -opc) + if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }} + return 1 + end + end + return 0 +end + +{{ range $v := .Completions }}{{ $v }} +{{ end }}` diff --git a/testdata/expected-doc-full.man b/testdata/expected-doc-full.man new file mode 100644 index 0000000..5190698 --- /dev/null +++ b/testdata/expected-doc-full.man @@ -0,0 +1,80 @@ +.nh +.TH greet(8) + +.SH Harrison + +.SH NAME +.PP +greet \- Some app + + +.SH SYNOPSIS +.PP +greet + +.PP +.RS + +.nf +[\-\-another\-flag|\-b] +[\-\-flag|\-\-fl|\-f]=[value] +[\-\-socket|\-s]=[value] + +.fi +.RE + + +.SH DESCRIPTION +.PP +app [first\_arg] [second\_arg] + +.PP +\fBUsage\fP: + +.PP +.RS + +.nf +greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] + +.fi +.RE + + +.SH GLOBAL OPTIONS +.PP +\fB\-\-another\-flag, \-b\fP: another usage text + +.PP +\fB\-\-flag, \-\-fl, \-f\fP="": + +.PP +\fB\-\-socket, \-s\fP="": some 'usage' text (default: value) + + +.SH COMMANDS +.SH config, c +.PP +another usage test + +.PP +\fB\-\-another\-flag, \-b\fP: another usage text + +.PP +\fB\-\-flag, \-\-fl, \-f\fP="": + +.SS sub\-config, s, ss +.PP +another usage test + +.PP +\fB\-\-sub\-command\-flag, \-s\fP: some usage text + +.PP +\fB\-\-sub\-flag, \-\-sub\-fl, \-s\fP="": + +.SH info, i, in +.PP +retrieve generic information + +.SH some\-command \ No newline at end of file diff --git a/testdata/expected-doc-full.md b/testdata/expected-doc-full.md new file mode 100644 index 0000000..23d7c23 --- /dev/null +++ b/testdata/expected-doc-full.md @@ -0,0 +1,62 @@ +% greet(8) + +% Harrison + +# NAME + +greet - Some app + +# SYNOPSIS + +greet + +``` +[--another-flag|-b] +[--flag|--fl|-f]=[value] +[--socket|-s]=[value] +``` + +# DESCRIPTION + +app [first_arg] [second_arg] + +**Usage**: + +``` +greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +``` + +# GLOBAL OPTIONS + +**--another-flag, -b**: another usage text + +**--flag, --fl, -f**="": + +**--socket, -s**="": some 'usage' text (default: value) + + +# COMMANDS + +## config, c + +another usage test + +**--another-flag, -b**: another usage text + +**--flag, --fl, -f**="": + +### sub-config, s, ss + +another usage test + +**--sub-command-flag, -s**: some usage text + +**--sub-flag, --sub-fl, -s**="": + +## info, i, in + +retrieve generic information + +## some-command + + diff --git a/testdata/expected-doc-no-commands.md b/testdata/expected-doc-no-commands.md new file mode 100644 index 0000000..18d8e35 --- /dev/null +++ b/testdata/expected-doc-no-commands.md @@ -0,0 +1,36 @@ +% greet(8) + +% Harrison + +# NAME + +greet - Some app + +# SYNOPSIS + +greet + +``` +[--another-flag|-b] +[--flag|--fl|-f]=[value] +[--socket|-s]=[value] +``` + +# DESCRIPTION + +app [first_arg] [second_arg] + +**Usage**: + +``` +greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +``` + +# GLOBAL OPTIONS + +**--another-flag, -b**: another usage text + +**--flag, --fl, -f**="": + +**--socket, -s**="": some 'usage' text (default: value) + diff --git a/testdata/expected-doc-no-flags.md b/testdata/expected-doc-no-flags.md new file mode 100644 index 0000000..8ce60fd --- /dev/null +++ b/testdata/expected-doc-no-flags.md @@ -0,0 +1,47 @@ +% greet(8) + +% Harrison + +# NAME + +greet - Some app + +# SYNOPSIS + +greet + +# DESCRIPTION + +app [first_arg] [second_arg] + +**Usage**: + +``` +greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +``` + +# COMMANDS + +## config, c + +another usage test + +**--another-flag, -b**: another usage text + +**--flag, --fl, -f**="": + +### sub-config, s, ss + +another usage test + +**--sub-command-flag, -s**: some usage text + +**--sub-flag, --sub-fl, -s**="": + +## info, i, in + +retrieve generic information + +## some-command + + diff --git a/testdata/expected-fish-full.fish b/testdata/expected-fish-full.fish new file mode 100644 index 0000000..b18d51e --- /dev/null +++ b/testdata/expected-fish-full.fish @@ -0,0 +1,28 @@ +# greet fish shell completion + +function __fish_greet_no_subcommand --description 'Test if there has been any subcommand yet' + for i in (commandline -opc) + if contains -- $i config c sub-config s ss info i in some-command + return 1 + end + end + return 0 +end + +complete -c greet -n '__fish_greet_no_subcommand' -l socket -s s -r -d 'some \'usage\' text' +complete -c greet -n '__fish_greet_no_subcommand' -f -l flag -s fl -s f -r +complete -c greet -n '__fish_greet_no_subcommand' -f -l another-flag -s b -d 'another usage text' +complete -c greet -n '__fish_greet_no_subcommand' -f -l help -s h -d 'show help' +complete -c greet -n '__fish_greet_no_subcommand' -f -l version -s v -d 'print the version' +complete -c greet -n '__fish_seen_subcommand_from config c' -f -l help -s h -d 'show help' +complete -r -c greet -n '__fish_greet_no_subcommand' -a 'config c' -d 'another usage test' +complete -c greet -n '__fish_seen_subcommand_from config c' -l flag -s fl -s f -r +complete -c greet -n '__fish_seen_subcommand_from config c' -f -l another-flag -s b -d 'another usage text' +complete -c greet -n '__fish_seen_subcommand_from sub-config s ss' -f -l help -s h -d 'show help' +complete -r -c greet -n '__fish_seen_subcommand_from config c' -a 'sub-config s ss' -d 'another usage test' +complete -c greet -n '__fish_seen_subcommand_from sub-config s ss' -f -l sub-flag -s sub-fl -s s -r +complete -c greet -n '__fish_seen_subcommand_from sub-config s ss' -f -l sub-command-flag -s s -d 'some usage text' +complete -c greet -n '__fish_seen_subcommand_from info i in' -f -l help -s h -d 'show help' +complete -r -c greet -n '__fish_greet_no_subcommand' -a 'info i in' -d 'retrieve generic information' +complete -c greet -n '__fish_seen_subcommand_from some-command' -f -l help -s h -d 'show help' +complete -r -c greet -n '__fish_greet_no_subcommand' -a 'some-command'