diff --git a/.github/.codecov.yml b/.github/.codecov.yml new file mode 100644 index 0000000..69cb760 --- /dev/null +++ b/.github/.codecov.yml @@ -0,0 +1 @@ +comment: false diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 9de4461..350d8fd 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -1,6 +1,10 @@ name: Run Tests on: + push: + branches: + - master + - v1 pull_request: branches: - master @@ -47,40 +51,38 @@ jobs: curl -L -o gfmrun.exe "https://github.com/urfave/gfmrun/releases/download/v1.2.14/gfmrun-windows-amd64-v1.2.14.exe" npm install markdown-toc - - name: Run Tests (v1) (Linux / MacOS) - if: contains(github.base_ref, 'v1') && matrix.os != 'windows-latest' + - name: Run Tests run: | go run build.go vet go run build.go test + + - name: Run Tests (v1) (Linux / MacOS) + if: contains(github.base_ref, 'v1') && matrix.os != 'windows-latest' + run: | go run build.go gfmrun docs/v1/manual.md go run build.go toc docs/v1/manual.md - name: Run Tests (v1) (Windows) if: contains(github.base_ref, 'v1') && matrix.os == 'windows-latest' run: | - go run build.go vet - go run build.go test go run build.go gfmrun docs\v1\manual.md go run build.go toc docs\v1\manual.md - name: Run Tests (v2) (Linux / MacOS) if: contains(github.base_ref, 'master') && matrix.os != 'windows-latest' run: | - go run build.go vet - go run build.go test go run build.go gfmrun docs/v2/manual.md go run build.go toc docs/v2/manual.md - name: Run Tests (v2) (Windows) if: contains(github.base_ref, 'master') && matrix.os == 'windows-latest' run: | - go run build.go vet - go run build.go test go run build.go gfmrun docs/v2/manual.md go run build.go toc docs/v2/manual.md - - name: Send Coverage Report - if: success() - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - run: bash <(curl -s https://codecov.io/bash) + - name: Upload coverage to Codecov + if: success() && matrix.go == 1.13 && matrix.os == 'ubuntu-latest' + uses: codecov/codecov-action@v1 + with: + token: 0a8cc73b-bb7c-480b-8626-38a461643761 + fail_ci_if_error: true diff --git a/README.md b/README.md index a8657bf..e7fb3d7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ 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) @@ -20,6 +19,12 @@ Usage documentation exists for each major version. Don't know what version you'r - `v2` - [./docs/v2/manual.md](./docs/v2/manual.md) - `v1` - [./docs/v1/manual.md](./docs/v1/manual.md) +## Installation + +Make sure you have a working Go environment. Go version 1.11+ is supported. [See the install instructions for Go](http://golang.org/doc/install.html). + +Go Modules are strongly recommended when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules). + ### Using `v2` releases ``` @@ -37,7 +42,7 @@ import ( ### Using `v1` releases ``` -$ go get github.com/urfave/cli +$ GO111MODULE=on go get github.com/urfave/cli ``` ```go @@ -48,11 +53,6 @@ import ( ... ``` -## Installation - -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). - ### GOPATH Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can @@ -64,5 +64,5 @@ export PATH=$PATH:$GOPATH/bin ### Supported platforms cli is tested against multiple versions of Go on Linux, and against the latest -released version of Go on OS X and Windows. For full details, see -[`./.travis.yml`](./.travis.yml) and [`./appveyor.yml`](./appveyor.yml). +released version of Go on OS X and Windows. This project uses Github Actions for +builds. For more build info, please look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/master/.github/workflows/cli.yml). diff --git a/app.go b/app.go index a62aa23..c04e9af 100644 --- a/app.go +++ b/app.go @@ -1,6 +1,7 @@ package cli import ( + "context" "flag" "fmt" "io" @@ -12,7 +13,7 @@ import ( ) var ( - changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md" + changeLogURL = "https://github.com/urfave/cli/blob/master/docs/CHANGELOG.md" appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", 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. "+ @@ -207,6 +208,13 @@ func (a *App) useShortOptionHandling() bool { // 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) { + return a.RunContext(context.Background(), arguments) +} + +// RunContext is like Run except it takes a Context that will be +// passed to its commands and sub-commands. Through this, you can +// propagate timeouts and cancellation requests +func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { a.Setup() // handle the completion flag separately from the flagset since @@ -224,7 +232,7 @@ func (a *App) Run(arguments []string) (err error) { err = parseIter(set, a, arguments[1:], shellComplete) nerr := normalizeFlags(a.Flags, set) - context := NewContext(a, set, nil) + context := NewContext(a, set, &Context{Context: ctx}) if nerr != nil { _, _ = fmt.Fprintln(a.Writer, nerr) _ = ShowAppHelp(context) diff --git a/app_test.go b/app_test.go index e675eb2..53e78b7 100644 --- a/app_test.go +++ b/app_test.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "io/ioutil" - "log" "os" "reflect" "strings" @@ -2121,7 +2120,7 @@ func TestWhenExitSubCommandWithCodeThenAppQuitUnexpectedly(t *testing.T) { var exitCodeFromExitErrHandler int app.ExitErrHandler = func(c *Context, err error) { if exitErr, ok := err.(ExitCoder); ok { - log.Print(exitErr) + t.Log(exitErr) exitCodeFromExitErrHandler = exitErr.ExitCode() } } diff --git a/appveyor.yml b/appveyor.yml index 6ae54b2..f1cae90 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,10 +20,9 @@ install: - go version - go env - go get github.com/urfave/gfmrun/cmd/gfmrun - - go get golang.org/x/tools/cmd/goimports - go mod tidy build_script: - go run build.go vet - go run build.go test - - go run build.go gfmrun docs/v1/manual.md \ No newline at end of file + - go run build.go gfmrun docs/v1/manual.md diff --git a/category.go b/category.go index d9e73a0..867e390 100644 --- a/category.go +++ b/category.go @@ -1,5 +1,6 @@ package cli +// CommandCategories interface allows for category manipulation type CommandCategories interface { // AddCommand adds a command to a category, creating a new category if necessary. AddCommand(category string, command *Command) diff --git a/context.go b/context.go index 13fb523..c0c526f 100644 --- a/context.go +++ b/context.go @@ -5,10 +5,7 @@ import ( "errors" "flag" "fmt" - "os" - "os/signal" "strings" - "syscall" ) // Context is a type that is passed through to @@ -31,19 +28,15 @@ func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { if parentCtx != nil { c.Context = parentCtx.Context c.shellComplete = parentCtx.shellComplete + if parentCtx.flagSet == nil { + parentCtx.flagSet = &flag.FlagSet{} + } } c.Command = &Command{} if c.Context == nil { - ctx, cancel := context.WithCancel(context.Background()) - go func() { - defer cancel() - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - <-sigs - }() - c.Context = ctx + c.Context = context.Background() } return c diff --git a/context_test.go b/context_test.go index 3cefcca..ccf8846 100644 --- a/context_test.go +++ b/context_test.go @@ -344,6 +344,81 @@ func TestContextPropagation(t *testing.T) { } } +func TestContextAttributeAccessing(t *testing.T) { + tdata := []struct { + testCase string + setBoolInput string + ctxBoolInput string + newContextInput *Context + }{ + { + testCase: "empty", + setBoolInput: "", + ctxBoolInput: "", + newContextInput: nil, + }, + { + testCase: "empty_with_background_context", + setBoolInput: "", + ctxBoolInput: "", + newContextInput: &Context{Context: context.Background()}, + }, + { + testCase: "empty_set_bool_and_present_ctx_bool", + setBoolInput: "", + ctxBoolInput: "ctx-bool", + newContextInput: nil, + }, + { + testCase: "present_set_bool_and_present_ctx_bool_with_background_context", + setBoolInput: "", + ctxBoolInput: "ctx-bool", + newContextInput: &Context{Context: context.Background()}, + }, + { + testCase: "present_set_bool_and_present_ctx_bool", + setBoolInput: "ctx-bool", + ctxBoolInput: "ctx-bool", + newContextInput: nil, + }, + { + testCase: "present_set_bool_and_present_ctx_bool_with_background_context", + setBoolInput: "ctx-bool", + ctxBoolInput: "ctx-bool", + newContextInput: &Context{Context: context.Background()}, + }, + { + testCase: "present_set_bool_and_different_ctx_bool", + setBoolInput: "ctx-bool", + ctxBoolInput: "not-ctx-bool", + newContextInput: nil, + }, + { + testCase: "present_set_bool_and_different_ctx_bool_with_background_context", + setBoolInput: "ctx-bool", + ctxBoolInput: "not-ctx-bool", + newContextInput: &Context{Context: context.Background()}, + }, + } + + for _, test := range tdata { + t.Run(test.testCase, func(t *testing.T) { + // setup + set := flag.NewFlagSet("some-flag-set-name", 0) + set.Bool(test.setBoolInput, false, "usage documentation") + ctx := NewContext(nil, set, test.newContextInput) + + // logic under test + value := ctx.Bool(test.ctxBoolInput) + + // assertions + if value != false { + t.Errorf("expected \"value\" to be false, but it was not") + } + }) + } +} + func TestCheckRequiredFlags(t *testing.T) { tdata := []struct { testCase string diff --git a/docs.go b/docs.go index a8f5de9..dc16fc8 100644 --- a/docs.go +++ b/docs.go @@ -48,8 +48,8 @@ func (a *App) writeDocTemplate(w io.Writer) error { return t.ExecuteTemplate(w, name, &cliTemplate{ App: a, Commands: prepareCommands(a.Commands, 0), - GlobalArgs: prepareArgsWithValues(a.Flags), - SynopsisArgs: prepareArgsSynopsis(a.Flags), + GlobalArgs: prepareArgsWithValues(a.VisibleFlags()), + SynopsisArgs: prepareArgsSynopsis(a.VisibleFlags()), }) } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 9aee16f..383507d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,33 @@ View [unreleased 2.X] series changes. +## [2.1.1] - 2019-12-24 + +### Fixed + +* Fixed a `Context` regression introduced in `v2.1.0` in [urfave/cli/pull/1014](https://github.com/urfave/cli/pull/1014) via [@lynncyrin](https://github.com/lynncyrin) + +## [2.1.0] - 2019-12-24 + +These release notes were written for the git hash [ae84df4cef4a2a6f1a0cb1d41ea0f3af8755e5a8](https://github.com/urfave/cli/tree/ae84df4cef4a2a6f1a0cb1d41ea0f3af8755e5a8) + +### Fixed + +* Fixed some golint errors in [urfave/cli/pull/988](https://github.com/urfave/cli/pull/988) via [@liamchampton](https://github.com/liamchampton) +* Fixed a panic with flag completion [urfave/cli/pull/946](https://github.com/urfave/cli/pull/946) via [@unRob](https://github.com/unRob) + +### Changed + +* Changed docs generation to use visible flags in [urfave/cli/pull/999](https://github.com/urfave/cli/pull/999) via [@subpop](https://github.com/subpop) +* Changed `App.Run` to use an optional context for timeouts and cancellation in [urfave/cli/pull/975](https://github.com/urfave/cli/pull/975) via [@marwan-at-work](https://github.com/marwan-at-work) +* Changed version info to be hidden if the user has not defined a version in [urfave/cli/pull/955](https://github.com/urfave/cli/pull/955) via [@asahasrabuddhe](https://github.com/asahasrabuddhe) +* Changed docs generation to take into account multiple authors in [urfave/cli/pull/900](https://github.com/urfave/cli/pull/900) via [@saschagrunert](https://github.com/saschagrunert) +* Changed context to expose a `Value` accessor in [urfave/cli/pull/741](https://github.com/urfave/cli/pull/741) via [@corruptmemory](https://github.com/corruptmemory) + +### Added + +* Added timestamp flag in [urfave/cli/pull/987](https://github.com/urfave/cli/pull/987) via [@drov0](https://github.com/drov0) + ## [2.0.0] - 2019-11-17 The V2 changes were all shipped in [urfave/cli/pull/892](https://github.com/urfave/cli/pull/892), which was created with the effort of over a dozen participants! They are: @@ -520,7 +547,9 @@ signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. ### Added - Initial implementation. -[unreleased 2.X]: https://github.com/urfave/cli/compare/v2.0.0...HEAD +[unreleased 2.X]: https://github.com/urfave/cli/compare/v2.1.1...HEAD +[2.1.1]: https://github.com/urfave/cli/compare/v2.1.0...v2.1.1 +[2.1.0]: https://github.com/urfave/cli/compare/v2.0.0...v2.1.0 [2.0.0]: https://github.com/urfave/cli/compare/v1.22.2...v2.0.0 [unreleased 1.22.X]: https://github.com/urfave/cli/compare/v1.22.2...v1 diff --git a/docs/v2/manual.md b/docs/v2/manual.md index 5e04d4e..cd96ff3 100644 --- a/docs/v2/manual.md +++ b/docs/v2/manual.md @@ -13,6 +13,7 @@ cli v2 manual + [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) + + [Required Flags](#required-flags) + [Default Values for help output](#default-values-for-help-output) + [Precedence](#precedence) * [Subcommands](#subcommands) @@ -641,6 +642,63 @@ func main() { } ``` +#### Required Flags + +You can make a flag required by setting the `Required` field to `true`. If a user +does not provide a required flag, they will be shown an error message. + +Take for example this app that reqiures the `lang` flag: + + +```go +package main + +import ( + "log" + "os" + "strings" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + Required: true, + }, + } + + app.Action = func(c *cli.Context) error { + var output string + if c.String("lang") == "spanish" { + output = "Hola" + } else { + output = "Hello" + } + fmt.Println(output) + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +If the app is run without the `lang` flag, the user will see the following message + +``` +Required flag "lang" not set +``` + #### Default Values for help output Sometimes it's useful to specify a flag's default help-text value within the flag declaration. This can be useful if the default value for a flag is a computed value. The default value can be set via the `DefaultText` struct field. @@ -1402,13 +1460,15 @@ func main() { cli.ShowVersion(c) fmt.Printf("%#v\n", c.App.Command("doo")) - if c.Bool("infinite") { - c.App.Run([]string{"app", "doo", "wop"}) - } - - if c.Bool("forevar") { - c.App.RunAsSubcommand(c) - } + // // uncomment when https://github.com/urfave/cli/pull/1014 is released + // if c.Bool("infinite") { + // c.App.Run([]string{"app", "doo", "wop"}) + // } + + // // uncomment when https://github.com/urfave/cli/pull/1014 is released + // if c.Bool("forevar") { + // c.App.RunAsSubcommand(c) + // } c.App.Setup() fmt.Printf("%#v\n", c.App.VisibleCategories()) fmt.Printf("%#v\n", c.App.VisibleCommands()) @@ -1424,28 +1484,29 @@ func main() { set := flag.NewFlagSet("contrive", 0) nc := cli.NewContext(c.App, set, c) - fmt.Printf("%#v\n", nc.Args()) - fmt.Printf("%#v\n", nc.Bool("nope")) - fmt.Printf("%#v\n", !nc.Bool("nerp")) - fmt.Printf("%#v\n", nc.Duration("howlong")) - fmt.Printf("%#v\n", nc.Float64("hay")) - fmt.Printf("%#v\n", nc.Generic("bloop")) - fmt.Printf("%#v\n", nc.Int64("bonk")) - fmt.Printf("%#v\n", nc.Int64Slice("burnks")) - fmt.Printf("%#v\n", nc.Int("bips")) - fmt.Printf("%#v\n", nc.IntSlice("blups")) - fmt.Printf("%#v\n", nc.String("snurt")) - fmt.Printf("%#v\n", nc.StringSlice("snurkles")) - fmt.Printf("%#v\n", nc.Uint("flub")) - fmt.Printf("%#v\n", nc.Uint64("florb")) - - fmt.Printf("%#v\n", nc.FlagNames()) - fmt.Printf("%#v\n", nc.IsSet("wat")) - fmt.Printf("%#v\n", nc.Set("wat", "nope")) - fmt.Printf("%#v\n", nc.NArg()) - fmt.Printf("%#v\n", nc.NumFlags()) - fmt.Printf("%#v\n", nc.Lineage()[1]) - + // // uncomment when https://github.com/urfave/cli/pull/1014 is released + // fmt.Printf("%#v\n", nc.Args()) + // fmt.Printf("%#v\n", nc.Bool("nope")) + // fmt.Printf("%#v\n", !nc.Bool("nerp")) + // fmt.Printf("%#v\n", nc.Duration("howlong")) + // fmt.Printf("%#v\n", nc.Float64("hay")) + // fmt.Printf("%#v\n", nc.Generic("bloop")) + // fmt.Printf("%#v\n", nc.Int64("bonk")) + // fmt.Printf("%#v\n", nc.Int64Slice("burnks")) + // fmt.Printf("%#v\n", nc.Int("bips")) + // fmt.Printf("%#v\n", nc.IntSlice("blups")) + // fmt.Printf("%#v\n", nc.String("snurt")) + // fmt.Printf("%#v\n", nc.StringSlice("snurkles")) + // fmt.Printf("%#v\n", nc.Uint("flub")) + // fmt.Printf("%#v\n", nc.Uint64("florb")) + + // // uncomment when https://github.com/urfave/cli/pull/1014 is released + // fmt.Printf("%#v\n", nc.FlagNames()) + // fmt.Printf("%#v\n", nc.IsSet("wat")) + // fmt.Printf("%#v\n", nc.Set("wat", "nope")) + // fmt.Printf("%#v\n", nc.NArg()) + // fmt.Printf("%#v\n", nc.NumFlags()) + // fmt.Printf("%#v\n", nc.Lineage()[1]) nc.Set("wat", "also-nope") ec := cli.Exit("ohwell", 86) diff --git a/docs_test.go b/docs_test.go index 3c15909..90240e7 100644 --- a/docs_test.go +++ b/docs_test.go @@ -23,6 +23,10 @@ func testApp() *App { Aliases: []string{"b"}, Usage: "another usage text", }, + &BoolFlag{ + Name: "hidden-flag", + Hidden: true, + }, } app.Commands = []*Command{{ Aliases: []string{"c"}, diff --git a/errors.go b/errors.go index be58903..344b436 100644 --- a/errors.go +++ b/errors.go @@ -48,6 +48,7 @@ func (m *multiError) Errors() []error { return errs } +// ErrorFormatter is the interface that will suitably format the error output type ErrorFormatter interface { Format(s fmt.State, verb rune) } diff --git a/flag_float64.go b/flag_float64.go index 2285547..31f06f3 100644 --- a/flag_float64.go +++ b/flag_float64.go @@ -18,11 +18,11 @@ type Float64Flag struct { Value float64 DefaultText string Destination *float64 - HasBeenSet bool + HasBeenSet bool } // IsSet returns whether or not the flag has been set through env or file -func (f *Float64Flag)IsSet() bool { +func (f *Float64Flag) IsSet() bool { return f.HasBeenSet } diff --git a/flag_path.go b/flag_path.go index d6b23c3..a322857 100644 --- a/flag_path.go +++ b/flag_path.go @@ -72,7 +72,7 @@ func (f *PathFlag) Apply(set *flag.FlagSet) error { return nil } -// String looks up the value of a local PathFlag, returns +// 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 { diff --git a/flag_test.go b/flag_test.go index 769f7bf..aaf3a6a 100644 --- a/flag_test.go +++ b/flag_test.go @@ -1678,3 +1678,58 @@ func TestInt64Slice_Serialized_Set(t *testing.T) { t.Fatalf("pre and post serialization do not match: %v != %v", sl0, sl1) } } + +func TestTimestamp_set(t *testing.T) { + ts := Timestamp{ + timestamp: nil, + hasBeenSet: false, + layout: "Jan 2, 2006 at 3:04pm (MST)", + } + + time1 := "Feb 3, 2013 at 7:54pm (PST)" + if err := ts.Set(time1); err != nil { + t.Fatalf("Failed to parse time %s with layout %s", time1, ts.layout) + } + if ts.hasBeenSet == false { + t.Fatalf("hasBeenSet is not true after setting a time") + } + + ts.hasBeenSet = false + ts.SetLayout(time.RFC3339) + time2 := "2006-01-02T15:04:05Z" + if err := ts.Set(time2); err != nil { + t.Fatalf("Failed to parse time %s with layout %s", time2, ts.layout) + } + if ts.hasBeenSet == false { + t.Fatalf("hasBeenSet is not true after setting a time") + } +} + +func TestTimestampFlagApply(t *testing.T) { + expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: time.RFC3339} + 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.Value.timestamp, expectedResult) +} + +func TestTimestampFlagApply_Fail_Parse_Wrong_Layout(t *testing.T) { + fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: "randomlayout"} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + 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 \"randomlayout\": cannot parse \"2006-01-02T15:04:05Z\" as \"randomlayout\"")) +} + +func TestTimestampFlagApply_Fail_Parse_Wrong_Time(t *testing.T) { + fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Layout: "Jan 2, 2006 at 3:04pm (MST)"} + set := flag.NewFlagSet("test", 0) + _ = fl.Apply(set) + + 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\"")) +} diff --git a/flag_timestamp.go b/flag_timestamp.go new file mode 100644 index 0000000..d24edcd --- /dev/null +++ b/flag_timestamp.go @@ -0,0 +1,152 @@ +package cli + +import ( + "flag" + "fmt" + "time" +) + +// Timestamp wrap to satisfy golang's flag interface. +type Timestamp struct { + timestamp *time.Time + hasBeenSet bool + layout string +} + +// Timestamp constructor +func NewTimestamp(timestamp time.Time) *Timestamp { + return &Timestamp{timestamp: ×tamp} +} + +// Set the timestamp value directly +func (t *Timestamp) SetTimestamp(value time.Time) { + if !t.hasBeenSet { + t.timestamp = &value + t.hasBeenSet = true + } +} + +// Set the timestamp string layout for future parsing +func (t *Timestamp) SetLayout(layout string) { + t.layout = layout +} + +// Parses the string value to timestamp +func (t *Timestamp) Set(value string) error { + timestamp, err := time.Parse(t.layout, value) + if err != nil { + return err + } + + t.timestamp = ×tamp + t.hasBeenSet = true + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (t *Timestamp) String() string { + return fmt.Sprintf("%#v", t.timestamp) +} + +// Value returns the timestamp value stored in the flag +func (t *Timestamp) Value() *time.Time { + return t.timestamp +} + +// Get returns the flag structure +func (t *Timestamp) Get() interface{} { + return *t +} + +// TimestampFlag is a flag with type time +type TimestampFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Layout string + Value *Timestamp + DefaultText string + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *TimestampFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *TimestampFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *TimestampFlag) Names() []string { + return flagNames(f) +} + +// IsRequired returns whether or not the flag is required +func (f *TimestampFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *TimestampFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *TimestampFlag) 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 *TimestampFlag) GetValue() string { + if f.Value != nil { + return f.Value.timestamp.String() + } + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *TimestampFlag) Apply(set *flag.FlagSet) error { + if f.Layout == "" { + return fmt.Errorf("timestamp Layout is required") + } + f.Value = &Timestamp{} + f.Value.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) + } + f.HasBeenSet = true + } + + for _, name := range f.Names() { + set.Var(f.Value, name, f.Usage) + } + return nil +} + +// Timestamp gets the timestamp from a flag name +func (c *Context) Timestamp(name string) *time.Time { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupTimestamp(name, fs) + } + return nil +} + +// Fetches the timestamp value from the local timestampWrap +func lookupTimestamp(name string, set *flag.FlagSet) *time.Time { + f := set.Lookup(name) + if f != nil { + return (f.Value.(*Timestamp)).Value() + } + return nil +} diff --git a/help.go b/help.go index aa5947d..c1e974a 100644 --- a/help.go +++ b/help.go @@ -138,7 +138,7 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden { continue } - for _, name := range flag.Names(){ + for _, name := range flag.Names() { name = strings.TrimSpace(name) // this will get total count utf8 letters in flag name count := utf8.RuneCountInString(name)