diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index bca4fea10d..b9680757c2 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,12 +1,10 @@ --- name: ask a question -about: ask us question - assume stackoverflow's guidelines apply here -title: 'q: ( your question title goes here )' +about: ask a question - assume stackoverflow's guidelines apply here +title: your question title goes here labels: 'kind/question, status/triage, area/v2' assignees: '' --- -## my question is... - -_**( Put the question text here )**_ +my question is... diff --git a/.github/ISSUE_TEMPLATE/v1-bug-report.md b/.github/ISSUE_TEMPLATE/v1-bug-report.md index aabfe820a6..1205d388be 100644 --- a/.github/ISSUE_TEMPLATE/v1-bug-report.md +++ b/.github/ISSUE_TEMPLATE/v1-bug-report.md @@ -1,28 +1,32 @@ --- name: v1 bug report about: Create a report to help us fix v1 bugs -title: 'v1 bug: ( your bug title goes here )' +title: 'your bug title goes here' labels: 'kind/bug, status/triage, area/v1' assignees: '' --- -## my urfave/cli version is +## My urfave/cli version is _**( Put the version of urfave/cli that you are using here )**_ ## Checklist -* [ ] Are you running the latest v1 release? The list of releases is [here](https://github.com/urfave/cli/releases). -* [ ] Did you check the manual for your release? The v1 manual is [here](https://github.com/urfave/cli/blob/master/docs/v1/manual.md) -* [ ] Did you perform a search about this problem? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching. +- [ ] Are you running the latest v1 release? The list of releases is [here](https://github.com/urfave/cli/releases). +- [ ] Did you check the manual for your release? The v1 manual is [here](https://github.com/urfave/cli/blob/main/docs/v1/manual.md). +- [ ] Did you perform a search about this problem? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching. ## Dependency Management -- [ ] My project is using go modules. -- [ ] My project is using vendoring. -- [ ] My project is automatically downloading the latest version. -- [ ] I am unsure of what my dependency management setup is. + + +- My project is using go modules. +- My project is using vendoring. +- My project is automatically downloading the latest version. +- I am unsure of what my dependency management setup is. ## Describe the bug @@ -34,23 +38,30 @@ Describe the steps or code required to reproduce the behavior ## Observed behavior -What did you see happen immediately after the reproduction steps above? +What did you see happen immediately after the reproduction steps +above? ## Expected behavior -What would you have expected to happen immediately after the reproduction steps above? +What would you have expected to happen immediately after the +reproduction steps above? ## Additional context Add any other context about the problem here. -If the issue relates to a specific open source Github repo, please link that repo here. +If the issue relates to a specific open source Github repo, please +link that repo here. -If you can reproduce this issue with a public CI system, please link a failing build here. +If you can reproduce this issue with a public CI system, please +link a failing build here. ## Want to fix this yourself? -We'd love to have more contributors on this project! If the fix for this bug is easily explained and very small, free free to create a pull request for it. You'll want to base the PR off the `v1` branch, all `v1` bug fix releases will be made from that branch. +We'd love to have more contributors on this project! If the fix for +this bug is easily explained and very small, free free to create a +pull request for it. You'll want to base the PR off the `v1` +branch, all `v1` bug fix releases will be made from that branch. ## Run `go version` and paste its output here diff --git a/.github/ISSUE_TEMPLATE/v2-bug-report.md b/.github/ISSUE_TEMPLATE/v2-bug-report.md index 9944769b3e..5c120e14f6 100644 --- a/.github/ISSUE_TEMPLATE/v2-bug-report.md +++ b/.github/ISSUE_TEMPLATE/v2-bug-report.md @@ -1,28 +1,32 @@ --- name: v2 bug report about: Create a report to help us fix v2 bugs -title: 'v2 bug: ( your bug title goes here )' +title: 'your bug title goes here' labels: 'kind/bug, area/v2, status/triage' assignees: '' --- -## my urfave/cli version is +## My urfave/cli version is _**( Put the version of urfave/cli that you are using here )**_ ## Checklist -* [ ] Are you running the latest v2 release? The list of releases is [here](https://github.com/urfave/cli/releases). -* [ ] Did you check the manual for your release? The v2 manual is [here](https://github.com/urfave/cli/blob/master/docs/v2/manual.md) -* [ ] Did you perform a search about this problem? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching. +- [ ] Are you running the latest v2 release? The list of releases is [here](https://github.com/urfave/cli/releases). +- [ ] Did you check the manual for your release? The v2 manual is [here](https://github.com/urfave/cli/blob/main/docs/v2/manual.md) +- [ ] Did you perform a search about this problem? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching. ## Dependency Management -- [ ] My project is using go modules. -- [ ] My project is using vendoring. -- [ ] My project is automatically downloading the latest version. -- [ ] I am unsure of what my dependency management setup is. + + +- My project is using go modules. +- My project is using vendoring. +- My project is automatically downloading the latest version. +- I am unsure of what my dependency management setup is. ## Describe the bug @@ -34,23 +38,30 @@ Describe the steps or code required to reproduce the behavior ## Observed behavior -What did you see happen immediately after the reproduction steps above? +What did you see happen immediately after the reproduction steps +above? ## Expected behavior -What would you have expected to happen immediately after the reproduction steps above? +What would you have expected to happen immediately after the +reproduction steps above? ## Additional context Add any other context about the problem here. -If the issue relates to a specific open source Github repo, please link that repo here. +If the issue relates to a specific open source Github repo, please +link that repo here. -If you can reproduce this issue with a public CI system, please link a failing build here. +If you can reproduce this issue with a public CI system, please +link a failing build here. ## Want to fix this yourself? -We'd love to have more contributors on this project! If the fix for this bug is easily explained and very small, free free to create a pull request for it. +We'd love to have more contributors on this project! If the fix for +this bug is easily explained and very small, free free to create a +pull request for it. + ## Run `go version` and paste its output here ``` diff --git a/.github/ISSUE_TEMPLATE/v2-feature-request.md b/.github/ISSUE_TEMPLATE/v2-feature-request.md index bc527b1c6e..2b1e727584 100644 --- a/.github/ISSUE_TEMPLATE/v2-feature-request.md +++ b/.github/ISSUE_TEMPLATE/v2-feature-request.md @@ -1,7 +1,7 @@ --- name: v2 feature request about: Suggest an improvement for v2 -title: 'v2 feature: ( your feature title goes here )' +title: 'your feature title goes here' labels: 'type/feature, area/v2, status/triage' assignees: '' @@ -10,16 +10,19 @@ assignees: '' ## Checklist * [ ] Are you running the latest v2 release? The list of releases is [here](https://github.com/urfave/cli/releases). -* [ ] Did you check the manual for your release? The v2 manual is [here](https://github.com/urfave/cli/blob/master/docs/v2/manual.md) +* [ ] Did you check the manual for your release? The v2 manual is [here](https://github.com/urfave/cli/blob/main/docs/v2/manual.md). * [ ] Did you perform a search about this feature? Here's the [Github guide](https://help.github.com/en/github/managing-your-work-on-github/using-search-to-filter-issues-and-pull-requests) about searching. ## What problem does this solve? A clear and concise description of what problem this feature would solve. For example: -- needing to type out the full flag name takes a long time, so I would like to suggest adding auto-complete -- I use (osx, windows, linux) and would like support for (some existing feature) to be extended to my platform -- the terminal output for a particular error case is confusing, and I think it could be improved +- needing to type out the full flag name takes a long time, so I + would like to suggest adding auto-complete +- I use (osx, windows, linux) and would like support for (some + existing feature) to be extended to my platform +- the terminal output for a particular error case is confusing, and + I think it could be improved ## Solution description @@ -27,4 +30,5 @@ A detailed description of what you want to happen. ## Describe alternatives you've considered -A clear and concise description of any alternative solutions or features you've considered. +A clear and concise description of any alternative solutions or +features you've considered. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4e76725765..47348f85ca 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,10 +8,14 @@ _(REQUIRED)_ -- [ ] bug -- [ ] cleanup -- [ ] documentation -- [ ] feature + + +- bug +- cleanup +- documentation +- feature ## What this PR does / why we need it: @@ -28,6 +32,7 @@ _(REQUIRED)_ ## Which issue(s) this PR fixes: _(REQUIRED)_ + @@ -53,7 +56,7 @@ Check each file for this and make the change. Shell command to find them all: `fgrep -rl github.com/urfave/cli *` -# Flag aliases are done differently. +# Flag aliases are done differently Change `Name: "foo, f"` to `Name: "foo", Aliases: []string{"f"}` @@ -181,6 +184,52 @@ Compiler messages you might see: cannot use c (type *cli.Command) as type cli.Command in append ``` +# GlobalString, GlobalBool and its likes are deprecated + +Use simply `String` instead of `GlobalString`, `Bool` instead of `GlobalBool` + +# BoolTFlag and BoolT are deprecated + +BoolTFlag was a Bool Flag with its default value set to true and BoolT was used to find any BoolTFlag used locally, so both are deprecated. + +* OLD: + +```go +cli.BoolTFlag{ + Name: FlagName, + Usage: FlagUsage, + EnvVar: "FLAG_ENV_VAR", +} +``` +* NEW: +```go +cli.BoolFlag{ + Name: FlagName, + Value: true, + Usage: FlagUsage, + EnvVar: "FLAG_ENV_VAR", +} +``` + + +# &cli.StringSlice{""} replaced with cli.NewStringSlice("") + +Example: + +* OLD: + +```go +Value: &cli.StringSlice{""}, +``` +* NEW: +```go +Value: cli.NewStringSlice(""), +} +``` +# Replace deprecated functions + +`cli.NewExitError()` is deprecated. Use `cli.Exit()` instead. ([Staticcheck](https://staticcheck.io/) detects this automatically and recommends replacement code.) + # Everything else Compile the code and work through any errors. Most should diff --git a/docs/v1/manual.md b/docs/v1/manual.md index 05ea37032e..dd22bdbdc7 100644 --- a/docs/v1/manual.md +++ b/docs/v1/manual.md @@ -612,7 +612,7 @@ given sources. Here is a more complete sample of a command using YAML support: ``` go diff --git a/docs/v2/manual.md b/docs/v2/manual.md index c39bfb9954..b480dd685f 100644 --- a/docs/v2/manual.md +++ b/docs/v2/manual.md @@ -30,6 +30,7 @@ cli v2 manual + [ZSH Support](#zsh-support) + [ZSH default auto-complete example](#zsh-default-auto-complete-example) + [ZSH custom auto-complete example](#zsh-custom-auto-complete-example) + + [PowerShell Support](#powershell-support) * [Generated Help Text](#generated-help-text) + [Customization](#customization-1) * [Version Flag](#version-flag) @@ -44,7 +45,7 @@ cli v2 manual There are a small set of breaking changes between v1 and v2. Converting is relatively straightforward and typically takes less than an hour. Specific steps are included in -[Migration Guide: v1 to v2](../migrate-v1-to-v2.md). +[Migration Guide: v1 to v2](../migrate-v1-to-v2.md). Also see the [pkg.go.dev docs](https://pkg.go.dev/github.com/urfave/cli/v2) for v2 API documentation. ## Getting Started @@ -425,15 +426,17 @@ import ( func main() { app := &cli.App{ Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "Language for the greeting", - }, - &cli.StringFlag{ - Name: "config, c", - Usage: "Load configuration from `FILE`", - }, + &cli.StringFlag{ + Name: "lang", + Aliases: []string{"l"}, + Value: "english", + Usage: "Language for the greeting", + }, + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "Load configuration from `FILE`", + }, }, Commands: []*cli.Command{ { @@ -511,7 +514,7 @@ func main() { ``` If `EnvVars` contains more than one string, the first environment variable that -resolves is used as the default. +resolves is used. ``` go @@ -645,7 +649,7 @@ func main() { app := &cli.App{ Action: func(c *cli.Context) error { - fmt.Println("yaml ist rad") + fmt.Println("--test value.*default: 0") return nil }, Before: altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")), @@ -670,8 +674,10 @@ Take for example this app that requires the `lang` flag: package main import ( + "fmt" "log" "os" + "github.com/urfave/cli/v2" ) @@ -1222,6 +1228,23 @@ source path/to/autocomplete/zsh_autocomplete #### ZSH custom auto-complete example ![](/docs/v2/images/custom-zsh-autocomplete.gif) +#### PowerShell Support +Auto-completion for PowerShell is also supported using the `autocomplete/powershell_autocomplete.ps1` +file included in this repo. + +Rename the script to `.ps1` and move it anywhere in your file system. +The location of script does not matter, only the file name of the script has to match +the your program's binary name. + +To activate it, enter `& path/to/autocomplete/.ps1` + +To persist across new shells, open the PowerShell profile (with `code $profile` or `notepad $profile`) +and add the line: +``` +& path/to/autocomplete/.ps1 +``` + + ### Generated Help Text The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked @@ -1309,7 +1332,8 @@ import ( func main() { cli.HelpFlag = &cli.BoolFlag{ - Name: "haaaaalp", Aliases: []string{"halp"}, + Name: "haaaaalp", + Aliases: []string{"halp"}, Usage: "HALP", EnvVars: []string{"SHOW_HALP", "HALPPLZ"}, } @@ -1344,7 +1368,8 @@ import ( func main() { cli.VersionFlag = &cli.BoolFlag{ - Name: "print-version", Aliases: []string{"V"}, + Name: "print-version", + Aliases: []string{"V"}, Usage: "print only the version", } diff --git a/docs_test.go b/docs_test.go index 46e38dc196..adccbbb9ad 100644 --- a/docs_test.go +++ b/docs_test.go @@ -2,6 +2,7 @@ package cli import ( "bytes" + "errors" "io/ioutil" "testing" ) @@ -66,8 +67,50 @@ func testApp() *App { }, { Name: "hidden-command", Hidden: true, + }, { + Aliases: []string{"u"}, + Flags: []Flag{ + &StringFlag{ + Name: "flag", + Aliases: []string{"fl", "f"}, + TakesFile: true, + }, + &BoolFlag{ + Name: "another-flag", + Aliases: []string{"b"}, + Usage: "another usage text", + }, + }, + Name: "usage", + Usage: "standard usage text", + UsageText: ` +Usage for the usage text +- formatted: Based on the specified ConfigMap and summon secrets.yml +- list: Inspect the environment for a specific process running on a Pod +- for_effect: Compare 'namespace' environment with 'local' + +` + "```" + ` +func() { ... } +` + "```" + ` + +Should be a part of the same code block +`, + Subcommands: []*Command{{ + Aliases: []string{"su"}, + Flags: []Flag{ + &BoolFlag{ + Name: "sub-command-flag", + Aliases: []string{"s"}, + Usage: "some usage text", + }, + }, + Name: "sub-usage", + Usage: "standard usage text", + UsageText: "Single line of UsageText", + }}, }} app.UsageText = "app [first_arg] [second_arg]" + app.Description = `Description of the application.` app.Usage = "Some app" app.Authors = []*Author{ {Name: "Harrison", Email: "harrison@lolwut.com"}, @@ -76,13 +119,13 @@ func testApp() *App { return app } -func expectFileContent(t *testing.T, file, expected string) { +func expectFileContent(t *testing.T, file, got string) { data, err := ioutil.ReadFile(file) // Ignore windows line endings // TODO: Replace with bytes.ReplaceAll when support for Go 1.11 is dropped data = bytes.Replace(data, []byte("\r\n"), []byte("\n"), -1) expect(t, err, nil) - expect(t, string(data), expected) + expect(t, got, string(data)) } func TestToMarkdownFull(t *testing.T) { @@ -136,6 +179,19 @@ func TestToMarkdownNoAuthors(t *testing.T) { expectFileContent(t, "testdata/expected-doc-no-authors.md", res) } +func TestToMarkdownNoUsageText(t *testing.T) { + // Given + app := testApp() + app.UsageText = "" + + // When + res, err := app.ToMarkdown() + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-doc-no-usagetext.md", res) +} + func TestToMan(t *testing.T) { // Given app := testApp() @@ -147,3 +203,137 @@ func TestToMan(t *testing.T) { expect(t, err, nil) expectFileContent(t, "testdata/expected-doc-full.man", res) } + +func TestToManParseError(t *testing.T) { + // Given + app := testApp() + + // When + // temporarily change the global variable for testing + tmp := MarkdownDocTemplate + MarkdownDocTemplate = `{{ .App.Name` + _, err := app.ToMan() + MarkdownDocTemplate = tmp + + // Then + expect(t, err, errors.New(`template: cli:1: unclosed action`)) +} + +func TestToManWithSection(t *testing.T) { + // Given + app := testApp() + + // When + res, err := app.ToManWithSection(8) + + // Then + expect(t, err, nil) + expectFileContent(t, "testdata/expected-doc-full.man", res) +} + +func Test_prepareUsageText(t *testing.T) { + t.Run("no UsageText provided", func(t *testing.T) { + // Given + cmd := Command{} + + // When + res := prepareUsageText(&cmd) + + // Then + expect(t, res, "") + }) + + t.Run("single line UsageText", func(t *testing.T) { + // Given + cmd := Command{UsageText: "Single line usage text"} + + // When + res := prepareUsageText(&cmd) + + // Then + expect(t, res, ">Single line usage text\n") + }) + + t.Run("multiline UsageText", func(t *testing.T) { + // Given + cmd := Command{ + UsageText: ` +Usage for the usage text +- Should be a part of the same code block +`, + } + + // When + res := prepareUsageText(&cmd) + + // Then + test := ` Usage for the usage text + - Should be a part of the same code block +` + expect(t, res, test) + }) + + t.Run("multiline UsageText has formatted embedded markdown", func(t *testing.T) { + // Given + cmd := Command{ + UsageText: ` +Usage for the usage text + +` + "```" + ` +func() { ... } +` + "```" + ` + +Should be a part of the same code block +`, + } + + // When + res := prepareUsageText(&cmd) + + // Then + test := ` Usage for the usage text + + ` + "```" + ` + func() { ... } + ` + "```" + ` + + Should be a part of the same code block +` + expect(t, res, test) + }) +} + +func Test_prepareUsage(t *testing.T) { + t.Run("no Usage provided", func(t *testing.T) { + // Given + cmd := Command{} + + // When + res := prepareUsage(&cmd, "") + + // Then + expect(t, res, "") + }) + + t.Run("simple Usage", func(t *testing.T) { + // Given + cmd := Command{Usage: "simple usage text"} + + // When + res := prepareUsage(&cmd, "") + + // Then + expect(t, res, cmd.Usage+"\n") + }) + + t.Run("simple Usage with UsageText", func(t *testing.T) { + // Given + cmd := Command{Usage: "simple usage text"} + + // When + res := prepareUsage(&cmd, "a non-empty string") + + // Then + expect(t, res, cmd.Usage+"\n\n") + }) +} diff --git a/errors.go b/errors.go index 751ef9b166..8f641fb64f 100644 --- a/errors.go +++ b/errors.go @@ -47,6 +47,28 @@ func (m *multiError) Errors() []error { return errs } +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 +} + // ErrorFormatter is the interface that will suitably format the error output type ErrorFormatter interface { Format(s fmt.State, verb rune) diff --git a/fish.go b/fish.go index 67122c9fe7..588e070eab 100644 --- a/fish.go +++ b/fish.go @@ -171,6 +171,10 @@ func fishAddFileFlag(flag Flag, completion *strings.Builder) { if f.TakesFile { return } + case *PathFlag: + if f.TakesFile { + return + } } completion.WriteString(" -f") } diff --git a/fish_test.go b/fish_test.go index a4c1871438..4ca8c47903 100644 --- a/fish_test.go +++ b/fish_test.go @@ -7,6 +7,10 @@ import ( func TestFishCompletion(t *testing.T) { // Given app := testApp() + app.Flags = append(app.Flags, &PathFlag{ + Name: "logfile", + TakesFile: true, + }) // When res, err := app.ToFishCompletion() diff --git a/flag.go b/flag.go index ad97c2d058..49682f2994 100644 --- a/flag.go +++ b/flag.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "flag" "fmt" "io/ioutil" @@ -118,6 +119,14 @@ type DocGenerationFlag interface { GetValue() string } +// VisibleFlag is an interface that allows to check if a flag is visible +type VisibleFlag interface { + Flag + + // IsVisible returns true if the flag is not hidden, otherwise false + IsVisible() bool +} + func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { set := flag.NewFlagSet(name, flag.ContinueOnError) @@ -130,11 +139,52 @@ func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { return set, nil } +func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { + switch ff.Value.(type) { + case Serializer: + _ = set.Set(name, ff.Value.(Serializer).Serialize()) + default: + _ = set.Set(name, ff.Value.String()) + } +} + +func normalizeFlags(flags []Flag, set *flag.FlagSet) error { + visited := make(map[string]bool) + set.Visit(func(f *flag.Flag) { + visited[f.Name] = true + }) + for _, f := range flags { + parts := f.Names() + if len(parts) == 1 { + continue + } + var ff *flag.Flag + for _, name := range parts { + name = strings.Trim(name, " ") + if visited[name] { + if ff != nil { + return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name) + } + ff = set.Lookup(name) + } + } + if ff == nil { + continue + } + for _, name := range parts { + name = strings.Trim(name, " ") + if !visited[name] { + copyFlag(name, ff, set) + } + } + } + return nil +} + func visibleFlags(fl []Flag) []Flag { var visible []Flag for _, f := range fl { - field := flagValue(f).FieldByName("Hidden") - if !field.IsValid() || !field.Bool() { + if vf, ok := f.(VisibleFlag); ok && vf.IsVisible() { visible = append(visible, f) } } @@ -359,7 +409,11 @@ func stringifySliceFlag(usage string, names, defaultVals []string) string { } usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) - return fmt.Sprintf("%s\t%s", prefixedNames(names, placeholder), usageWithDefault) + multiInputString := "(accepts multiple inputs)" + if usageWithDefault != "" { + multiInputString = "\t" + multiInputString + } + return fmt.Sprintf("%s\t%s%s", prefixedNames(names, placeholder), usageWithDefault, multiInputString) } func hasFlag(flags []Flag, fl Flag) bool { @@ -380,8 +434,10 @@ func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool) } } for _, fileVar := range strings.Split(filePath, ",") { - if data, err := ioutil.ReadFile(fileVar); err == nil { - return string(data), true + if fileVar != "" { + if data, err := ioutil.ReadFile(fileVar); err == nil { + return string(data), true + } } } return "", false diff --git a/flag_bool.go b/flag_bool.go index bc9ea35d08..8bd582094f 100644 --- a/flag_bool.go +++ b/flag_bool.go @@ -58,6 +58,11 @@ func (f *BoolFlag) GetValue() string { return "" } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *BoolFlag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -87,7 +92,7 @@ func (f *BoolFlag) Apply(set *flag.FlagSet) error { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupBool(name, fs) } return false diff --git a/flag_duration.go b/flag_duration.go index 22a2e67201..28f3978051 100644 --- a/flag_duration.go +++ b/flag_duration.go @@ -58,6 +58,11 @@ func (f *DurationFlag) GetValue() string { return f.Value.String() } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *DurationFlag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -86,7 +91,7 @@ func (f *DurationFlag) Apply(set *flag.FlagSet) error { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupDuration(name, fs) } return 0 diff --git a/flag_float64.go b/flag_float64.go index 91c778c873..352f02bd3f 100644 --- a/flag_float64.go +++ b/flag_float64.go @@ -58,12 +58,16 @@ func (f *Float64Flag) GetValue() string { return fmt.Sprintf("%f", f.Value) } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Float64Flag) IsVisible() bool { + return !f.Hidden +} + // 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) - + valFloat, err := strconv.ParseFloat(val, 64) if err != nil { return fmt.Errorf("could not parse %q as float64 value for flag %s: %s", val, f.Name, err) } @@ -87,7 +91,7 @@ func (f *Float64Flag) Apply(set *flag.FlagSet) error { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupFloat64(name, fs) } return 0 diff --git a/flag_float64_slice.go b/flag_float64_slice.go index 706ee6cd4b..385732e17c 100644 --- a/flag_float64_slice.go +++ b/flag_float64_slice.go @@ -19,6 +19,16 @@ func NewFloat64Slice(defaults ...float64) *Float64Slice { return &Float64Slice{slice: append([]float64{}, defaults...)} } +// clone allocate a copy of self object +func (f *Float64Slice) clone() *Float64Slice { + n := &Float64Slice{ + slice: make([]float64, len(f.slice)), + hasBeenSet: f.hasBeenSet, + } + copy(n.slice, f.slice) + return n +} + // 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 { @@ -117,6 +127,11 @@ func (f *Float64SliceFlag) GetValue() string { return "" } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Float64SliceFlag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -129,15 +144,19 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { } } + // Set this to false so that we reset the slice if we then set values from + // flags that have already been set by the environment. + f.Value.hasBeenSet = false f.HasBeenSet = true } } + if f.Value == nil { + f.Value = &Float64Slice{} + } + copyValue := f.Value.clone() for _, name := range f.Names() { - if f.Value == nil { - f.Value = &Float64Slice{} - } - set.Var(f.Value, name, f.Usage) + set.Var(copyValue, name, f.Usage) } return nil @@ -146,7 +165,7 @@ func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupFloat64Slice(name, fs) } return nil diff --git a/flag_generic.go b/flag_generic.go index b0c8ff44d2..fdf586d127 100644 --- a/flag_generic.go +++ b/flag_generic.go @@ -66,6 +66,11 @@ func (f *GenericFlag) GetValue() string { return "" } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *GenericFlag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -89,7 +94,7 @@ func (f GenericFlag) Apply(set *flag.FlagSet) error { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupGeneric(name, fs) } return nil diff --git a/flag_int.go b/flag_int.go index ac39d4a9e4..1ebe3569c1 100644 --- a/flag_int.go +++ b/flag_int.go @@ -58,6 +58,11 @@ func (f *IntFlag) GetValue() string { return fmt.Sprintf("%d", f.Value) } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *IntFlag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -87,7 +92,7 @@ func (f *IntFlag) Apply(set *flag.FlagSet) error { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupInt(name, fs) } return 0 diff --git a/flag_int64.go b/flag_int64.go index e09991269b..ecf0e9ef6f 100644 --- a/flag_int64.go +++ b/flag_int64.go @@ -58,6 +58,11 @@ func (f *Int64Flag) GetValue() string { return fmt.Sprintf("%d", f.Value) } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Int64Flag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -86,7 +91,7 @@ func (f *Int64Flag) Apply(set *flag.FlagSet) error { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupInt64(name, fs) } return 0 diff --git a/flag_int64_slice.go b/flag_int64_slice.go index 6c7fd9376d..f5c6939639 100644 --- a/flag_int64_slice.go +++ b/flag_int64_slice.go @@ -19,6 +19,16 @@ func NewInt64Slice(defaults ...int64) *Int64Slice { return &Int64Slice{slice: append([]int64{}, defaults...)} } +// clone allocate a copy of self object +func (i *Int64Slice) clone() *Int64Slice { + n := &Int64Slice{ + slice: make([]int64, len(i.slice)), + hasBeenSet: i.hasBeenSet, + } + copy(n.slice, i.slice) + return n +} + // 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 { @@ -118,6 +128,11 @@ func (f *Int64SliceFlag) GetValue() string { return "" } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Int64SliceFlag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -129,14 +144,18 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { } } + // Set this to false so that we reset the slice if we then set values from + // flags that have already been set by the environment. + f.Value.hasBeenSet = false f.HasBeenSet = true } + if f.Value == nil { + f.Value = &Int64Slice{} + } + copyValue := f.Value.clone() for _, name := range f.Names() { - if f.Value == nil { - f.Value = &Int64Slice{} - } - set.Var(f.Value, name, f.Usage) + set.Var(copyValue, name, f.Usage) } return nil @@ -145,7 +164,10 @@ func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { // Int64Slice looks up the value of a local Int64SliceFlag, returns // nil if not found func (c *Context) Int64Slice(name string) []int64 { - return lookupInt64Slice(name, c.flagSet) + if fs := c.lookupFlagSet(name); fs != nil { + return lookupInt64Slice(name, fs) + } + return nil } func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { diff --git a/flag_int_slice.go b/flag_int_slice.go index 4e0afc0210..94c668e9f1 100644 --- a/flag_int_slice.go +++ b/flag_int_slice.go @@ -19,6 +19,16 @@ func NewIntSlice(defaults ...int) *IntSlice { return &IntSlice{slice: append([]int{}, defaults...)} } +// clone allocate a copy of self object +func (i *IntSlice) clone() *IntSlice { + n := &IntSlice{ + slice: make([]int, len(i.slice)), + hasBeenSet: i.hasBeenSet, + } + copy(n.slice, i.slice) + return n +} + // 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) { @@ -129,6 +139,11 @@ func (f *IntSliceFlag) GetValue() string { return "" } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *IntSliceFlag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -140,14 +155,18 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { } } + // Set this to false so that we reset the slice if we then set values from + // flags that have already been set by the environment. + f.Value.hasBeenSet = false f.HasBeenSet = true } + if f.Value == nil { + f.Value = &IntSlice{} + } + copyValue := f.Value.clone() for _, name := range f.Names() { - if f.Value == nil { - f.Value = &IntSlice{} - } - set.Var(f.Value, name, f.Usage) + set.Var(copyValue, name, f.Usage) } return nil @@ -156,8 +175,8 @@ func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { // 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) + if fs := c.lookupFlagSet(name); fs != nil { + return lookupIntSlice(name, fs) } return nil } diff --git a/flag_path.go b/flag_path.go index 8070dc4b0b..b3aa9191d3 100644 --- a/flag_path.go +++ b/flag_path.go @@ -54,6 +54,11 @@ func (f *PathFlag) GetValue() string { return f.Value } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *PathFlag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -75,7 +80,7 @@ func (f *PathFlag) Apply(set *flag.FlagSet) error { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupPath(name, fs) } diff --git a/flag_string.go b/flag_string.go index 400bb532e7..aad4c43a9c 100644 --- a/flag_string.go +++ b/flag_string.go @@ -55,6 +55,11 @@ func (f *StringFlag) GetValue() string { return f.Value } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *StringFlag) IsVisible() bool { + return !f.Hidden +} + // Apply populates the flag given the flag set and environment func (f *StringFlag) Apply(set *flag.FlagSet) error { if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { @@ -76,7 +81,7 @@ func (f *StringFlag) Apply(set *flag.FlagSet) error { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupString(name, fs) } return "" @@ -85,10 +90,7 @@ func (c *Context) String(name string) string { 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 "" - } + parsed := f.Value.String() return parsed } return "" diff --git a/flag_string_slice.go b/flag_string_slice.go index 74cf0a51f2..5269643105 100644 --- a/flag_string_slice.go +++ b/flag_string_slice.go @@ -18,6 +18,16 @@ func NewStringSlice(defaults ...string) *StringSlice { return &StringSlice{slice: append([]string{}, defaults...)} } +// clone allocate a copy of self object +func (s *StringSlice) clone() *StringSlice { + n := &StringSlice{ + slice: make([]string, len(s.slice)), + hasBeenSet: s.hasBeenSet, + } + copy(n.slice, s.slice) + return n +} + // Set appends the string value to the list of values func (s *StringSlice) Set(value string) error { if !s.hasBeenSet { @@ -114,6 +124,11 @@ func (f *StringSliceFlag) GetValue() string { return "" } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *StringSliceFlag) IsVisible() bool { + return !f.Hidden +} + // Apply populates the flag given the flag set and environment func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { @@ -124,7 +139,9 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { } if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - f.Value = &StringSlice{} + if f.Value == nil { + f.Value = &StringSlice{} + } destination := f.Value if f.Destination != nil { destination = f.Destination @@ -142,17 +159,15 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { f.HasBeenSet = true } + if f.Value == nil { + f.Value = &StringSlice{} + } + setValue := f.Destination + if f.Destination == nil { + setValue = f.Value.clone() + } for _, name := range f.Names() { - if f.Value == nil { - f.Value = &StringSlice{} - } - - if f.Destination != nil { - set.Var(f.Destination, name, f.Usage) - continue - } - - set.Var(f.Value, name, f.Usage) + set.Var(setValue, name, f.Usage) } return nil @@ -161,7 +176,7 @@ func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupStringSlice(name, fs) } return nil diff --git a/flag_test.go b/flag_test.go index ced5514714..c563d6f346 100644 --- a/flag_test.go +++ b/flag_test.go @@ -52,15 +52,21 @@ func TestBoolFlagApply_SetsAllNames(t *testing.T) { } func TestFlagsFromEnv(t *testing.T) { + newSetFloat64Slice := func(defaults ...float64) Float64Slice { + s := NewFloat64Slice(defaults...) + s.hasBeenSet = false + return *s + } + newSetIntSlice := func(defaults ...int) IntSlice { s := NewIntSlice(defaults...) - s.hasBeenSet = true + s.hasBeenSet = false return *s } newSetInt64Slice := func(defaults ...int64) Int64Slice { s := NewInt64Slice(defaults...) - s.hasBeenSet = true + s.hasBeenSet = false return *s } @@ -96,6 +102,9 @@ func TestFlagsFromEnv(t *testing.T) { {"1.2", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2" as int value for flag seconds: .*`}, {"foobar", 0, &IntFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int value for flag seconds: .*`}, + {"1.0,2", newSetFloat64Slice(1, 2), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, + {"foobar", newSetFloat64Slice(), &Float64SliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "\[\]float64{}" as float64 slice value for flag seconds: .*`}, + {"1,2", newSetIntSlice(1, 2), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, ""}, {"1.2,2", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "1.2,2" as int slice value for flag seconds: .*`}, {"foobar", newSetIntSlice(), &IntSliceFlag{Name: "seconds", EnvVars: []string{"SECONDS"}}, `could not parse "foobar" as int slice value for flag seconds: .*`}, @@ -340,11 +349,11 @@ var stringSliceFlagTests = []struct { 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\")"}, + {"foo", nil, NewStringSlice(""), "--foo value\t(accepts multiple inputs)"}, + {"f", nil, NewStringSlice(""), "-f value\t(accepts multiple inputs)"}, + {"f", nil, NewStringSlice("Lipstick"), "-f value\t(default: \"Lipstick\")\t(accepts multiple inputs)"}, + {"test", nil, NewStringSlice("Something"), "--test value\t(default: \"Something\")\t(accepts multiple inputs)"}, + {"dee", []string{"d"}, NewStringSlice("Inka", "Dinka", "dooo"), "--dee value, -d value\t(default: \"Inka\", \"Dinka\", \"dooo\")\t(accepts multiple inputs)"}, } func TestStringSliceFlagHelpOutput(t *testing.T) { @@ -386,6 +395,20 @@ func TestStringSliceFlagApply_SetsAllNames(t *testing.T) { expect(t, err, nil) } +func TestStringSliceFlagApply_UsesEnvValues(t *testing.T) { + defer resetEnv(os.Environ()) + os.Clearenv() + _ = os.Setenv("MY_GOAT", "vincent van goat,scape goat") + var val StringSlice + fl := StringSliceFlag{Name: "goat", EnvVars: []string{"MY_GOAT"}, Value: &val} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse(nil) + expect(t, err, nil) + expect(t, val.Value(), NewStringSlice("vincent van goat", "scape goat").Value()) +} + func TestStringSliceFlagApply_DefaultValueWithDestination(t *testing.T) { defValue := []string{"UA", "US"} @@ -616,9 +639,9 @@ var intSliceFlagTests = []struct { value *IntSlice expected string }{ - {"heads", nil, NewIntSlice(), "--heads value\t"}, - {"H", nil, NewIntSlice(), "-H value\t"}, - {"H", []string{"heads"}, NewIntSlice(9, 3), "-H value, --heads value\t(default: 9, 3)"}, + {"heads", nil, NewIntSlice(), "--heads value\t(accepts multiple inputs)"}, + {"H", nil, NewIntSlice(), "-H value\t(accepts multiple inputs)"}, + {"H", []string{"heads"}, NewIntSlice(9, 3), "-H value, --heads value\t(default: 9, 3)\t(accepts multiple inputs)"}, } func TestIntSliceFlagHelpOutput(t *testing.T) { @@ -660,16 +683,55 @@ func TestIntSliceFlagApply_SetsAllNames(t *testing.T) { expect(t, err, nil) } +func TestIntSliceFlagApply_ParentContext(t *testing.T) { + _ = (&App{ + Flags: []Flag{ + &IntSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewIntSlice(1, 2, 3)}, + }, + Commands: []*Command{ + { + Name: "child", + Action: func(ctx *Context) error { + expected := []int{1, 2, 3} + if !reflect.DeepEqual(ctx.IntSlice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.IntSlice("numbers")) + } + if !reflect.DeepEqual(ctx.IntSlice("n"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.IntSlice("n")) + } + return nil + }, + }, + }, + }).Run([]string{"run", "child"}) +} + +func TestIntSliceFlag_SetFromParentContext(t *testing.T) { + fl := &IntSliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewIntSlice(1, 2, 3, 4)} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + ctx := &Context{ + parentContext: &Context{ + flagSet: set, + }, + flagSet: flag.NewFlagSet("empty", 0), + } + expected := []int{1, 2, 3, 4} + if !reflect.DeepEqual(ctx.IntSlice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.IntSlice("numbers")) + } +} + var int64SliceFlagTests = []struct { name string aliases []string value *Int64Slice expected string }{ - {"heads", nil, NewInt64Slice(), "--heads value\t"}, - {"H", nil, NewInt64Slice(), "-H value\t"}, + {"heads", nil, NewInt64Slice(), "--heads value\t(accepts multiple inputs)"}, + {"H", nil, NewInt64Slice(), "-H value\t(accepts multiple inputs)"}, {"heads", []string{"H"}, NewInt64Slice(int64(2), int64(17179869184)), - "--heads value, -H value\t(default: 2, 17179869184)"}, + "--heads value, -H value\t(default: 2, 17179869184)\t(accepts multiple inputs)"}, } func TestInt64SliceFlagHelpOutput(t *testing.T) { @@ -702,6 +764,60 @@ func TestInt64SliceFlagWithEnvVarHelpOutput(t *testing.T) { } } +func TestInt64SliceFlagApply_ParentContext(t *testing.T) { + _ = (&App{ + Flags: []Flag{ + &Int64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewInt64Slice(1, 2, 3)}, + }, + Commands: []*Command{ + { + Name: "child", + Action: func(ctx *Context) error { + expected := []int64{1, 2, 3} + if !reflect.DeepEqual(ctx.Int64Slice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("numbers")) + } + if !reflect.DeepEqual(ctx.Int64Slice("n"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("n")) + } + return nil + }, + }, + }, + }).Run([]string{"run", "child"}) +} + +func TestInt64SliceFlag_SetFromParentContext(t *testing.T) { + fl := &Int64SliceFlag{Name: "numbers", Aliases: []string{"n"}, Value: NewInt64Slice(1, 2, 3, 4)} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + ctx := &Context{ + parentContext: &Context{ + flagSet: set, + }, + flagSet: flag.NewFlagSet("empty", 0), + } + expected := []int64{1, 2, 3, 4} + if !reflect.DeepEqual(ctx.Int64Slice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("numbers")) + } +} +func TestInt64SliceFlag_ReturnNil(t *testing.T) { + fl := &Int64SliceFlag{} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + ctx := &Context{ + parentContext: &Context{ + flagSet: set, + }, + flagSet: flag.NewFlagSet("empty", 0), + } + expected := []int64(nil) + if !reflect.DeepEqual(ctx.Int64Slice("numbers"), expected) { + t.Errorf("child context unable to view parent flag: %v != %v", expected, ctx.Int64Slice("numbers")) + } +} + var float64FlagTests = []struct { name string expected string @@ -757,10 +873,10 @@ var float64SliceFlagTests = []struct { value *Float64Slice expected string }{ - {"heads", nil, NewFloat64Slice(), "--heads value\t"}, - {"H", nil, NewFloat64Slice(), "-H value\t"}, + {"heads", nil, NewFloat64Slice(), "--heads value\t(accepts multiple inputs)"}, + {"H", nil, NewFloat64Slice(), "-H value\t(accepts multiple inputs)"}, {"heads", []string{"H"}, NewFloat64Slice(0.1234, -10.5), - "--heads value, -H value\t(default: 0.1234, -10.5)"}, + "--heads value, -H value\t(default: 0.1234, -10.5)\t(accepts multiple inputs)"}, } func TestFloat64SliceFlagHelpOutput(t *testing.T) { @@ -1866,3 +1982,80 @@ func TestTimestampFlagApply_Fail_Parse_Wrong_Time(t *testing.T) { err := set.Parse([]string{"--time", "2006-01-02T15:04:05Z"}) expect(t, err, fmt.Errorf("invalid value \"2006-01-02T15:04:05Z\" for flag -time: parsing time \"2006-01-02T15:04:05Z\" as \"Jan 2, 2006 at 3:04pm (MST)\": cannot parse \"2006-01-02T15:04:05Z\" as \"Jan\"")) } + +type flagDefaultTestCase struct { + name string + flag Flag + toParse []string + expect string +} + +func TestFlagDefaultValue(t *testing.T) { + cases := []*flagDefaultTestCase{ + &flagDefaultTestCase{ + name: "stringSclice", + flag: &StringSliceFlag{Name: "flag", Value: NewStringSlice("default1", "default2")}, + toParse: []string{"--flag", "parsed"}, + expect: `--flag value (default: "default1", "default2") (accepts multiple inputs)`, + }, + &flagDefaultTestCase{ + name: "float64Sclice", + flag: &Float64SliceFlag{Name: "flag", Value: NewFloat64Slice(1.1, 2.2)}, + toParse: []string{"--flag", "13.3"}, + expect: `--flag value (default: 1.1, 2.2) (accepts multiple inputs)`, + }, + &flagDefaultTestCase{ + name: "int64Sclice", + flag: &Int64SliceFlag{Name: "flag", Value: NewInt64Slice(1, 2)}, + toParse: []string{"--flag", "13"}, + expect: `--flag value (default: 1, 2) (accepts multiple inputs)`, + }, + &flagDefaultTestCase{ + name: "intSclice", + flag: &IntSliceFlag{Name: "flag", Value: NewIntSlice(1, 2)}, + toParse: []string{"--flag", "13"}, + expect: `--flag value (default: 1, 2) (accepts multiple inputs)`, + }, + &flagDefaultTestCase{ + name: "string", + flag: &StringFlag{Name: "flag", Value: "default"}, + toParse: []string{"--flag", "parsed"}, + expect: `--flag value (default: "default")`, + }, + &flagDefaultTestCase{ + name: "bool", + flag: &BoolFlag{Name: "flag", Value: true}, + toParse: []string{"--flag", "false"}, + expect: `--flag (default: true)`, + }, + &flagDefaultTestCase{ + name: "uint64", + flag: &Uint64Flag{Name: "flag", Value: 1}, + toParse: []string{"--flag", "13"}, + expect: `--flag value (default: 1)`, + }, + } + for i, v := range cases { + set := flag.NewFlagSet("test", 0) + set.SetOutput(ioutil.Discard) + _ = v.flag.Apply(set) + if err := set.Parse(v.toParse); err != nil { + t.Error(err) + } + if got := v.flag.String(); got != v.expect { + t.Errorf("TestFlagDefaultValue %d %s\nexpect:%s\ngot:%s", i, v.name, v.expect, got) + } + } +} + +func TestTimestampFlagApply_WithDestination(t *testing.T) { + var destination Timestamp + expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: time.RFC3339, Destination: &destination} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + err := set.Parse([]string{"--time", "2006-01-02T15:04:05Z"}) + expect(t, err, nil) + expect(t, *fl.Destination.timestamp, expectedResult) +} diff --git a/flag_timestamp.go b/flag_timestamp.go index 0382a6b9dc..7458a79b6a 100644 --- a/flag_timestamp.go +++ b/flag_timestamp.go @@ -71,6 +71,7 @@ type TimestampFlag struct { Value *Timestamp DefaultText string HasBeenSet bool + Destination *Timestamp } // IsSet returns whether or not the flag has been set through env or file @@ -113,6 +114,11 @@ func (f *TimestampFlag) GetValue() string { return "" } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *TimestampFlag) IsVisible() bool { + return !f.Hidden +} + // Apply populates the flag given the flag set and environment func (f *TimestampFlag) Apply(set *flag.FlagSet) error { if f.Layout == "" { @@ -123,6 +129,10 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error { } f.Value.SetLayout(f.Layout) + if f.Destination != nil { + f.Destination.SetLayout(f.Layout) + } + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { if err := f.Value.Set(val); err != nil { return fmt.Errorf("could not parse %q as timestamp value for flag %s: %s", val, f.Name, err) @@ -131,6 +141,11 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error { } for _, name := range f.Names() { + if f.Destination != nil { + set.Var(f.Destination, name, f.Usage) + continue + } + set.Var(f.Value, name, f.Usage) } return nil @@ -138,7 +153,7 @@ func (f *TimestampFlag) Apply(set *flag.FlagSet) error { // Timestamp gets the timestamp from a flag name func (c *Context) Timestamp(name string) *time.Time { - if fs := lookupFlagSet(name, c); fs != nil { + if fs := c.lookupFlagSet(name); fs != nil { return lookupTimestamp(name, fs) } return nil diff --git a/flag_uint.go b/flag_uint.go index 2e5e76b0ea..23a70a7f90 100644 --- a/flag_uint.go +++ b/flag_uint.go @@ -52,6 +52,11 @@ func (f *UintFlag) GetUsage() string { return f.Usage } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *UintFlag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -86,7 +91,7 @@ func (f *UintFlag) GetValue() string { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupUint(name, fs) } return 0 diff --git a/flag_uint64.go b/flag_uint64.go index 8fc3289d82..a2df024e18 100644 --- a/flag_uint64.go +++ b/flag_uint64.go @@ -52,6 +52,11 @@ func (f *Uint64Flag) GetUsage() string { return f.Usage } +// IsVisible returns true if the flag is not hidden, otherwise false +func (f *Uint64Flag) IsVisible() bool { + return !f.Hidden +} + // 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 { @@ -86,7 +91,7 @@ func (f *Uint64Flag) GetValue() string { // 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 { + if fs := c.lookupFlagSet(name); fs != nil { return lookupUint64(name, fs) } return 0 diff --git a/funcs.go b/funcs.go index 474c48faf9..842b4aa99c 100644 --- a/funcs.go +++ b/funcs.go @@ -17,7 +17,7 @@ type ActionFunc func(*Context) error // CommandNotFoundFunc is executed if the proper command cannot be found type CommandNotFoundFunc func(*Context, string) -// OnUsageErrorFunc is executed if an usage error occurs. This is useful for displaying +// OnUsageErrorFunc is executed if a usage error occurs. This is useful for displaying // customized usage error messages. This function is able to replace the // original error messages. If this function is not set, the "Incorrect usage" // is displayed and the execution is interrupted. diff --git a/go.mod b/go.mod index c38d41c14b..7a4b88bbf0 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,11 @@ module github.com/urfave/cli/v2 -go 1.11 +go 1.18 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 + github.com/BurntSushi/toml v1.1.0 + github.com/cpuguy83/go-md2man/v2 v2.0.1 + gopkg.in/yaml.v2 v2.4.0 ) + +require github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index ef121ff5db..bde502bd2e 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,12 @@ 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= +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/help.go b/help.go index 95ba188945..03cead43fc 100644 --- a/help.go +++ b/help.go @@ -72,13 +72,13 @@ func ShowAppHelpAndExit(c *Context, exitCode int) { // ShowAppHelp is an action that displays the help. func ShowAppHelp(c *Context) error { - template := c.App.CustomAppHelpTemplate - if template == "" { - template = AppHelpTemplate + tpl := c.App.CustomAppHelpTemplate + if tpl == "" { + tpl = AppHelpTemplate } if c.App.ExtraInfo == nil { - HelpPrinter(c.App.Writer, template, c.App) + HelpPrinter(c.App.Writer, tpl, c.App) return nil } @@ -87,7 +87,7 @@ func ShowAppHelp(c *Context) error { "ExtraInfo": c.App.ExtraInfo, } } - HelpPrinterCustom(c.App.Writer, template, c.App, customAppData()) + HelpPrinterCustom(c.App.Writer, tpl, c.App, customAppData()) return nil } @@ -215,6 +215,12 @@ func ShowCommandHelp(ctx *Context, command string) error { return nil } +// ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits with exit code. +func ShowSubcommandHelpAndExit(c *Context, exitCode int) { + _ = ShowSubcommandHelp(c) + os.Exit(exitCode) +} + // ShowSubcommandHelp prints help for the given subcommand func ShowSubcommandHelp(c *Context) error { if c == nil { @@ -264,7 +270,10 @@ func ShowCommandCompletions(ctx *Context, command string) { // allow using arbitrary functions in template rendering. func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { funcMap := template.FuncMap{ - "join": strings.Join, + "join": strings.Join, + "indent": indent, + "nindent": nindent, + "trim": strings.TrimSpace, } for key, value := range customFuncs { funcMap[key] = value @@ -367,3 +376,12 @@ func checkCommandCompletions(c *Context, name string) bool { ShowCommandCompletions(c, name) return true } + +func indent(spaces int, v string) string { + pad := strings.Repeat(" ", spaces) + return pad + strings.Replace(v, "\n", "\n"+pad, -1) +} + +func nindent(spaces int, v string) string { + return "\n" + indent(spaces, v) +} diff --git a/help_test.go b/help_test.go index 5f292b77e5..8dd262d56c 100644 --- a/help_test.go +++ b/help_test.go @@ -54,6 +54,22 @@ func Test_ShowAppHelp_HideVersion(t *testing.T) { } } +func Test_ShowAppHelp_MultiLineDescription(t *testing.T) { + output := new(bytes.Buffer) + app := &App{Writer: output} + + app.HideVersion = true + app.Description = "multi\n line" + + c := NewContext(app, nil, nil) + + _ = ShowAppHelp(c) + + if !bytes.Contains(output.Bytes(), []byte("DESCRIPTION:\n multi\n line")) { + t.Errorf("expected\n%s\nto include\n%s", output.String(), "DESCRIPTION:\n multi\n line") + } +} + func Test_Help_Custom_Flags(t *testing.T) { oldFlag := HelpFlag defer func() { @@ -495,6 +511,36 @@ func TestShowSubcommandHelp_CommandUsageText(t *testing.T) { } } +func TestShowSubcommandHelp_MultiLine_CommandUsageText(t *testing.T) { + app := &App{ + Commands: []*Command{ + { + Name: "frobbly", + UsageText: `This is a +multi +line +UsageText`, + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + + _ = app.Run([]string{"foo", "frobbly", "--help"}) + + expected := `USAGE: + This is a + multi + line + UsageText +` + + if !strings.Contains(output.String(), expected) { + t.Errorf("expected output to include usage text; got: %q", output.String()) + } +} + func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) { app := &App{ Commands: []*Command{ @@ -519,6 +565,40 @@ func TestShowSubcommandHelp_SubcommandUsageText(t *testing.T) { } } +func TestShowSubcommandHelp_MultiLine_SubcommandUsageText(t *testing.T) { + app := &App{ + Commands: []*Command{ + { + Name: "frobbly", + Subcommands: []*Command{ + { + Name: "bobbly", + UsageText: `This is a +multi +line +UsageText`, + }, + }, + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + _ = app.Run([]string{"foo", "frobbly", "bobbly", "--help"}) + + expected := `USAGE: + This is a + multi + line + UsageText +` + + if !strings.Contains(output.String(), expected) { + t.Errorf("expected output to include usage text; got: %q", output.String()) + } +} + func TestShowAppHelp_HiddenCommand(t *testing.T) { app := &App{ Commands: []*Command{ @@ -764,6 +844,56 @@ VERSION: } } +func TestShowAppHelp_UsageText(t *testing.T) { + app := &App{ + UsageText: "This is a sinlge line of UsageText", + Commands: []*Command{ + { + Name: "frobbly", + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + + _ = app.Run([]string{"foo"}) + + if !strings.Contains(output.String(), "This is a sinlge line of UsageText") { + t.Errorf("expected output to include usage text; got: %q", output.String()) + } +} + +func TestShowAppHelp_MultiLine_UsageText(t *testing.T) { + app := &App{ + UsageText: `This is a +multi +line +App UsageText`, + Commands: []*Command{ + { + Name: "frobbly", + }, + }, + } + + output := &bytes.Buffer{} + app.Writer = output + + _ = app.Run([]string{"foo"}) + + expected := `USAGE: + This is a + multi + line + App UsageText +` + + if !strings.Contains(output.String(), expected) { + t.Errorf("expected output to include usage text; got: %q", output.String()) + } +} + func TestHideHelpCommand(t *testing.T) { app := &App{ HideHelpCommand: true, diff --git a/helpers_test.go b/helpers_test.go index 9217e89e8e..9ecd8e18ae 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -3,24 +3,17 @@ package cli import ( "os" "reflect" - "runtime" - "strings" "testing" ) -var ( - wd, _ = os.Getwd() -) - func init() { _ = os.Setenv("CLI_TEMPLATE_REPANIC", "1") } func expect(t *testing.T, a interface{}, b interface{}) { - _, fn, line, _ := runtime.Caller(1) - fn = strings.Replace(fn, wd+"/", "", -1) + t.Helper() if !reflect.DeepEqual(a, b) { - t.Errorf("(%s:%d) Expected %v (type %v) - Got %v (type %v)", fn, line, b, reflect.TypeOf(b), a, reflect.TypeOf(a)) + t.Errorf("Expected %v (type %v) - Got %v (type %v)", b, reflect.TypeOf(b), a, reflect.TypeOf(a)) } } diff --git a/internal/build/build.go b/internal/build/build.go index 197cfa5b60..9fd2cf9e82 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -193,8 +193,8 @@ func checkBinarySizeActionFunc(c *cli.Context) (err error) { cliBuiltFilePath = "./internal/example-cli/built-example" helloSourceFilePath = "./internal/example-hello-world/example-hello-world.go" helloBuiltFilePath = "./internal/example-hello-world/built-example" - desiredMinBinarySize = 2.0 - desiredMaxBinarySize = 2.1 + desiredMinBinarySize = 1.9 + desiredMaxBinarySize = 2.2 badNewsEmoji = "🚨" goodNewsEmoji = "✨" checksPassedEmoji = "✅" diff --git a/template.go b/template.go index aee3e0494f..39fa4db080 100644 --- a/template.go +++ b/template.go @@ -7,13 +7,13 @@ 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}} + {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{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}} + {{.Description | nindent 3 | trim}}{{end}}{{if len .Authors}} AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: {{range $index, $author := .Authors}}{{if $index}} @@ -39,13 +39,13 @@ 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}} + {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{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}} + {{.Description | nindent 3 | trim}}{{end}}{{if .VisibleFlags}} OPTIONS: {{range .VisibleFlags}}{{.}} @@ -59,10 +59,10 @@ var SubcommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} + {{if .UsageText}}{{.UsageText | nindent 3 | trim}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} DESCRIPTION: - {{.Description}}{{end}} + {{.Description | nindent 3 | trim}}{{end}} COMMANDS:{{range .VisibleCategories}}{{if .Name}} {{.Name}}:{{range .VisibleCommands}} @@ -74,9 +74,9 @@ OPTIONS: {{end}}{{end}} ` -var MarkdownDocTemplate = `% {{ .App.Name }} 8 +var MarkdownDocTemplate = `{{if gt .SectionNum 0}}% {{ .App.Name }} {{ .SectionNum }} -# NAME +{{end}}# NAME {{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }} @@ -86,16 +86,18 @@ var MarkdownDocTemplate = `% {{ .App.Name }} 8 {{ if .SynopsisArgs }} ` + "```" + ` {{ range $v := .SynopsisArgs }}{{ $v }}{{ end }}` + "```" + ` -{{ end }}{{ if .App.UsageText }} +{{ end }}{{ if .App.Description }} # DESCRIPTION -{{ .App.UsageText }} +{{ .App.Description }} {{ end }} **Usage**: -` + "```" + ` +` + "```" + `{{ if .App.UsageText }} +{{ .App.UsageText }} +{{ else }} {{ .App.Name }} [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] -` + "```" + ` +{{ end }}` + "```" + ` {{ if .GlobalArgs }} # GLOBAL OPTIONS {{ range $v := .GlobalArgs }} diff --git a/testdata/expected-doc-full.man b/testdata/expected-doc-full.man index 2bea50731e..a2131f0f55 100644 --- a/testdata/expected-doc-full.man +++ b/testdata/expected-doc-full.man @@ -3,7 +3,7 @@ .SH NAME .PP -greet \- Some app +greet - Some app .SH SYNOPSIS @@ -14,9 +14,9 @@ greet .RS .nf -[\-\-another\-flag|\-b] -[\-\-flag|\-\-fl|\-f]=[value] -[\-\-socket|\-s]=[value] +[--another-flag|-b] +[--flag|--fl|-f]=[value] +[--socket|-s]=[value] .fi .RE @@ -24,7 +24,7 @@ greet .SH DESCRIPTION .PP -app [first\_arg] [second\_arg] +Description of the application. .PP \fBUsage\fP: @@ -33,7 +33,7 @@ app [first\_arg] [second\_arg] .RS .nf -greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +app [first_arg] [second_arg] .fi .RE @@ -41,13 +41,13 @@ greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] .SH GLOBAL OPTIONS .PP -\fB\-\-another\-flag, \-b\fP: another usage text +\fB--another-flag, -b\fP: another usage text .PP -\fB\-\-flag, \-\-fl, \-f\fP="": +\fB--flag, --fl, -f\fP="": .PP -\fB\-\-socket, \-s\fP="": some 'usage' text (default: value) +\fB--socket, -s\fP="": some 'usage' text (default: value) .SH COMMANDS @@ -56,23 +56,65 @@ greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] another usage test .PP -\fB\-\-another\-flag, \-b\fP: another usage text +\fB--another-flag, -b\fP: another usage text .PP -\fB\-\-flag, \-\-fl, \-f\fP="": +\fB--flag, --fl, -f\fP="": -.SS sub\-config, s, ss +.SS sub-config, s, ss .PP another usage test .PP -\fB\-\-sub\-command\-flag, \-s\fP: some usage text +\fB--sub-command-flag, -s\fP: some usage text .PP -\fB\-\-sub\-flag, \-\-sub\-fl, \-s\fP="": +\fB--sub-flag, --sub-fl, -s\fP="": .SH info, i, in .PP retrieve generic information -.SH some\-command \ No newline at end of file +.SH some-command +.SH usage, u +.PP +standard usage text + +.PP +.RS + +.nf +Usage for the usage text +- formatted: Based on the specified ConfigMap and summon secrets.yml +- list: Inspect the environment for a specific process running on a Pod +- for_effect: Compare 'namespace' environment with 'local' + +``` +func() { ... } +``` + +Should be a part of the same code block + +.fi +.RE + +.PP +\fB--another-flag, -b\fP: another usage text + +.PP +\fB--flag, --fl, -f\fP="": + +.SS sub-usage, su +.PP +standard usage text + +.PP +.RS + +.PP +Single line of UsageText + +.RE + +.PP +\fB--sub-command-flag, -s\fP: some usage text diff --git a/testdata/expected-doc-full.md b/testdata/expected-doc-full.md index f272274061..80ff6a7a68 100644 --- a/testdata/expected-doc-full.md +++ b/testdata/expected-doc-full.md @@ -1,5 +1,3 @@ -% greet 8 - # NAME greet - Some app @@ -16,12 +14,12 @@ greet # DESCRIPTION -app [first_arg] [second_arg] +Description of the application. **Usage**: ``` -greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +app [first_arg] [second_arg] ``` # GLOBAL OPTIONS @@ -58,3 +56,29 @@ retrieve generic information ## some-command +## usage, u + +standard usage text + + Usage for the usage text + - formatted: Based on the specified ConfigMap and summon secrets.yml + - list: Inspect the environment for a specific process running on a Pod + - for_effect: Compare 'namespace' environment with 'local' + + ``` + func() { ... } + ``` + + Should be a part of the same code block + +**--another-flag, -b**: another usage text + +**--flag, --fl, -f**="": + +### sub-usage, su + +standard usage text + +>Single line of UsageText + +**--sub-command-flag, -s**: some usage text diff --git a/testdata/expected-doc-no-authors.md b/testdata/expected-doc-no-authors.md index f272274061..80ff6a7a68 100644 --- a/testdata/expected-doc-no-authors.md +++ b/testdata/expected-doc-no-authors.md @@ -1,5 +1,3 @@ -% greet 8 - # NAME greet - Some app @@ -16,12 +14,12 @@ greet # DESCRIPTION -app [first_arg] [second_arg] +Description of the application. **Usage**: ``` -greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +app [first_arg] [second_arg] ``` # GLOBAL OPTIONS @@ -58,3 +56,29 @@ retrieve generic information ## some-command +## usage, u + +standard usage text + + Usage for the usage text + - formatted: Based on the specified ConfigMap and summon secrets.yml + - list: Inspect the environment for a specific process running on a Pod + - for_effect: Compare 'namespace' environment with 'local' + + ``` + func() { ... } + ``` + + Should be a part of the same code block + +**--another-flag, -b**: another usage text + +**--flag, --fl, -f**="": + +### sub-usage, su + +standard usage text + +>Single line of UsageText + +**--sub-command-flag, -s**: some usage text diff --git a/testdata/expected-doc-no-commands.md b/testdata/expected-doc-no-commands.md index adaedb43e0..4db2a375ec 100644 --- a/testdata/expected-doc-no-commands.md +++ b/testdata/expected-doc-no-commands.md @@ -1,5 +1,3 @@ -% greet 8 - # NAME greet - Some app @@ -16,12 +14,12 @@ greet # DESCRIPTION -app [first_arg] [second_arg] +Description of the application. **Usage**: ``` -greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +app [first_arg] [second_arg] ``` # GLOBAL OPTIONS diff --git a/testdata/expected-doc-no-flags.md b/testdata/expected-doc-no-flags.md index 2994593e8a..33d1275a93 100644 --- a/testdata/expected-doc-no-flags.md +++ b/testdata/expected-doc-no-flags.md @@ -1,5 +1,3 @@ -% greet 8 - # NAME greet - Some app @@ -10,12 +8,12 @@ greet # DESCRIPTION -app [first_arg] [second_arg] +Description of the application. **Usage**: ``` -greet [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +app [first_arg] [second_arg] ``` # COMMANDS @@ -43,3 +41,29 @@ retrieve generic information ## some-command +## usage, u + +standard usage text + + Usage for the usage text + - formatted: Based on the specified ConfigMap and summon secrets.yml + - list: Inspect the environment for a specific process running on a Pod + - for_effect: Compare 'namespace' environment with 'local' + + ``` + func() { ... } + ``` + + Should be a part of the same code block + +**--another-flag, -b**: another usage text + +**--flag, --fl, -f**="": + +### sub-usage, su + +standard usage text + +>Single line of UsageText + +**--sub-command-flag, -s**: some usage text diff --git a/testdata/expected-doc-no-usagetext.md b/testdata/expected-doc-no-usagetext.md new file mode 100644 index 0000000000..d09b69fb6b --- /dev/null +++ b/testdata/expected-doc-no-usagetext.md @@ -0,0 +1,84 @@ +# NAME + +greet - Some app + +# SYNOPSIS + +greet + +``` +[--another-flag|-b] +[--flag|--fl|-f]=[value] +[--socket|-s]=[value] +``` + +# DESCRIPTION + +Description of the application. + +**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 + + +## usage, u + +standard usage text + + Usage for the usage text + - formatted: Based on the specified ConfigMap and summon secrets.yml + - list: Inspect the environment for a specific process running on a Pod + - for_effect: Compare 'namespace' environment with 'local' + + ``` + func() { ... } + ``` + + Should be a part of the same code block + +**--another-flag, -b**: another usage text + +**--flag, --fl, -f**="": + +### sub-usage, su + +standard usage text + +>Single line of UsageText + +**--sub-command-flag, -s**: some usage text diff --git a/testdata/expected-fish-full.fish b/testdata/expected-fish-full.fish index b18d51e8fe..cc449c5f46 100644 --- a/testdata/expected-fish-full.fish +++ b/testdata/expected-fish-full.fish @@ -2,7 +2,7 @@ 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 + if contains -- $i config c sub-config s ss info i in some-command usage u sub-usage su return 1 end end @@ -12,6 +12,7 @@ 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' -l logfile -r 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' @@ -26,3 +27,10 @@ complete -c greet -n '__fish_seen_subcommand_from info i in' -f -l help -s h -d 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' +complete -c greet -n '__fish_seen_subcommand_from usage u' -f -l help -s h -d 'show help' +complete -r -c greet -n '__fish_greet_no_subcommand' -a 'usage u' -d 'standard usage text' +complete -c greet -n '__fish_seen_subcommand_from usage u' -l flag -s fl -s f -r +complete -c greet -n '__fish_seen_subcommand_from usage u' -f -l another-flag -s b -d 'another usage text' +complete -c greet -n '__fish_seen_subcommand_from sub-usage su' -f -l help -s h -d 'show help' +complete -r -c greet -n '__fish_seen_subcommand_from usage u' -a 'sub-usage su' -d 'standard usage text' +complete -c greet -n '__fish_seen_subcommand_from sub-usage su' -f -l sub-command-flag -s s -d 'some usage text'