diff --git a/.github/stale.yml b/.github/stale.yml index a3282a2727..0cc30077be 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -14,6 +14,7 @@ onlyLabels: [] exemptLabels: - pinned - security + - "help wanted" - "kind/maintenance" # Set to true to ignore issues in a project (defaults to false) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 06dbb80836..edeebdf765 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - go: [1.13, 1.14, 1.15] + go: [1.14, 1.15, 1.16] name: ${{ matrix.os }} @ Go ${{ matrix.go }} runs-on: ${{ matrix.os }} steps: @@ -38,7 +38,7 @@ jobs: ref: ${{ github.ref }} - name: GOFMT Check - if: matrix.go == 1.15 && matrix.os == 'ubuntu-latest' + if: matrix.go == 1.16 && matrix.os == 'ubuntu-latest' run: test -z $(gofmt -l .) - name: vet @@ -51,19 +51,19 @@ jobs: run: go run internal/build/build.go check-binary-size - name: Upload coverage to Codecov - if: success() && matrix.go == 1.14 && matrix.os == 'ubuntu-latest' + if: success() && matrix.go == 1.16 && matrix.os == 'ubuntu-latest' uses: codecov/codecov-action@v1 with: - token: 0a8cc73b-bb7c-480b-8626-38a461643761 fail_ci_if_error: true test-docs: name: test-docs runs-on: ubuntu-latest steps: - - name: Set up Go 1.15 + - name: Set up Go uses: actions/setup-go@v1 with: + # Currently fails on 1.16 go-version: 1.15 - name: Use Node.js 12.x diff --git a/app.go b/app.go index 2c97251650..5c616e6f51 100644 --- a/app.go +++ b/app.go @@ -278,7 +278,7 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { return nil } - cerr := checkRequiredFlags(a.Flags, context) + cerr := context.checkRequiredFlags(a.Flags) if cerr != nil { _ = ShowAppHelp(context) return cerr @@ -397,7 +397,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } } - cerr := checkRequiredFlags(a.Flags, context) + cerr := context.checkRequiredFlags(a.Flags) if cerr != nil { _ = ShowSubcommandHelp(context) return cerr diff --git a/command.go b/command.go index 3503a555b0..3477686bb3 100644 --- a/command.go +++ b/command.go @@ -127,7 +127,7 @@ func (c *Command) Run(ctx *Context) (err error) { return nil } - cerr := checkRequiredFlags(c.Flags, context) + cerr := context.checkRequiredFlags(c.Flags) if cerr != nil { _ = ShowCommandHelp(context, c.Name) return cerr @@ -227,6 +227,7 @@ func (c *Command) startApp(ctx *Context) error { } app.Usage = c.Usage + app.UsageText = c.UsageText app.Description = c.Description app.ArgsUsage = c.ArgsUsage diff --git a/context.go b/context.go index 65e0d1e4cb..94cbb65909 100644 --- a/context.go +++ b/context.go @@ -2,9 +2,7 @@ package cli import ( "context" - "errors" "flag" - "fmt" "strings" ) @@ -53,20 +51,18 @@ func (c *Context) Set(name, value string) error { // IsSet determines if the flag was actually set func (c *Context) IsSet(name string) bool { - if fs := lookupFlagSet(name, c); fs != nil { - if fs := lookupFlagSet(name, c); fs != nil { - isSet := false - fs.Visit(func(f *flag.Flag) { - if f.Name == name { - isSet = true - } - }) - if isSet { - return true + if fs := c.lookupFlagSet(name); fs != nil { + isSet := false + fs.Visit(func(f *flag.Flag) { + if f.Name == name { + isSet = true } + }) + if isSet { + return true } - f := lookupFlag(name, c) + f := c.lookupFlag(name) if f == nil { return false } @@ -108,7 +104,7 @@ func (c *Context) Lineage() []*Context { // Value returns the value of the flag corresponding to `name` func (c *Context) Value(name string) interface{} { - if fs := lookupFlagSet(name, c); fs != nil { + if fs := c.lookupFlagSet(name); fs != nil { return fs.Lookup(name).Value.(flag.Getter).Get() } return nil @@ -125,7 +121,7 @@ func (c *Context) NArg() int { return c.Args().Len() } -func lookupFlag(name string, ctx *Context) Flag { +func (ctx *Context) lookupFlag(name string) Flag { for _, c := range ctx.Lineage() { if c.Command == nil { continue @@ -153,7 +149,7 @@ func lookupFlag(name string, ctx *Context) Flag { return nil } -func lookupFlagSet(name string, ctx *Context) *flag.FlagSet { +func (ctx *Context) lookupFlagSet(name string) *flag.FlagSet { for _, c := range ctx.Lineage() { if f := c.flagSet.Lookup(name); f != nil { return c.flagSet @@ -163,89 +159,7 @@ func lookupFlagSet(name string, ctx *Context) *flag.FlagSet { return 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 makeFlagNameVisitor(names *[]string) func(*flag.Flag) { - return func(f *flag.Flag) { - nameParts := strings.Split(f.Name, ",") - name := strings.TrimSpace(nameParts[0]) - - for _, part := range nameParts { - part = strings.TrimSpace(part) - if len(part) > len(name) { - name = part - } - } - - if name != "" { - *names = append(*names, name) - } - } -} - -type requiredFlagsErr interface { - error - getMissingFlags() []string -} - -type errRequiredFlags struct { - missingFlags []string -} - -func (e *errRequiredFlags) Error() string { - numberOfMissingFlags := len(e.missingFlags) - if numberOfMissingFlags == 1 { - return fmt.Sprintf("Required flag %q not set", e.missingFlags[0]) - } - joinedMissingFlags := strings.Join(e.missingFlags, ", ") - return fmt.Sprintf("Required flags %q not set", joinedMissingFlags) -} - -func (e *errRequiredFlags) getMissingFlags() []string { - return e.missingFlags -} - -func checkRequiredFlags(flags []Flag, context *Context) requiredFlagsErr { +func (context *Context) checkRequiredFlags(flags []Flag) requiredFlagsErr { var missingFlags []string for _, f := range flags { if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { @@ -274,3 +188,21 @@ func checkRequiredFlags(flags []Flag, context *Context) requiredFlagsErr { return nil } + +func makeFlagNameVisitor(names *[]string) func(*flag.Flag) { + return func(f *flag.Flag) { + nameParts := strings.Split(f.Name, ",") + name := strings.TrimSpace(nameParts[0]) + + for _, part := range nameParts { + part = strings.TrimSpace(part) + if len(part) > len(name) { + name = part + } + } + + if name != "" { + *names = append(*names, name) + } + } +} diff --git a/context_test.go b/context_test.go index 35feefedec..b37876c033 100644 --- a/context_test.go +++ b/context_test.go @@ -316,13 +316,13 @@ func TestContext_lookupFlagSet(t *testing.T) { _ = set.Parse([]string{"--local-flag"}) _ = parentSet.Parse([]string{"--top-flag"}) - fs := lookupFlagSet("top-flag", ctx) + fs := ctx.lookupFlagSet("top-flag") expect(t, fs, parentCtx.flagSet) - fs = lookupFlagSet("local-flag", ctx) + fs = ctx.lookupFlagSet("local-flag") expect(t, fs, ctx.flagSet) - if fs := lookupFlagSet("frob", ctx); fs != nil { + if fs := ctx.lookupFlagSet("frob"); fs != nil { t.Fail() } } @@ -576,7 +576,7 @@ func TestCheckRequiredFlags(t *testing.T) { ctx.Command.Flags = test.flags // logic under test - err := checkRequiredFlags(test.flags, ctx) + err := ctx.checkRequiredFlags(test.flags) // assertions if test.expectedAnError && err == nil { 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/flag.go b/flag.go index aff8d5be63..a693386524 100644 --- a/flag.go +++ b/flag.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "flag" "fmt" "io/ioutil" @@ -130,6 +131,48 @@ 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 { diff --git a/flag_bool.go b/flag_bool.go index bc9ea35d08..85270e44ba 100644 --- a/flag_bool.go +++ b/flag_bool.go @@ -87,7 +87,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..7b59a38651 100644 --- a/flag_duration.go +++ b/flag_duration.go @@ -86,7 +86,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..d2a6458657 100644 --- a/flag_float64.go +++ b/flag_float64.go @@ -87,7 +87,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 b625ca1756..d4e4029584 100644 --- a/flag_float64_slice.go +++ b/flag_float64_slice.go @@ -157,7 +157,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..d6800a858d 100644 --- a/flag_generic.go +++ b/flag_generic.go @@ -89,7 +89,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..e9da3fa47c 100644 --- a/flag_int.go +++ b/flag_int.go @@ -87,7 +87,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..6c5545883c 100644 --- a/flag_int64.go +++ b/flag_int64.go @@ -86,7 +86,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 d2352c08fa..6f9fd6b885 100644 --- a/flag_int64_slice.go +++ b/flag_int64_slice.go @@ -156,7 +156,7 @@ 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 { - if fs := lookupFlagSet(name, c); fs != nil { + if fs := c.lookupFlagSet(name); fs != nil { return lookupInt64Slice(name, fs) } return nil diff --git a/flag_int_slice.go b/flag_int_slice.go index 018d56b625..ec44b70610 100644 --- a/flag_int_slice.go +++ b/flag_int_slice.go @@ -167,7 +167,7 @@ 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 { + 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..37c6f27d35 100644 --- a/flag_path.go +++ b/flag_path.go @@ -75,7 +75,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..a43f7c20cb 100644 --- a/flag_string.go +++ b/flag_string.go @@ -76,7 +76,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 "" diff --git a/flag_string_slice.go b/flag_string_slice.go index 0b45ab5044..e9e27bbacd 100644 --- a/flag_string_slice.go +++ b/flag_string_slice.go @@ -171,7 +171,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 0f8e12adef..e46270d034 100644 --- a/flag_test.go +++ b/flag_test.go @@ -2038,3 +2038,15 @@ func TestFlagDefaultValue(t *testing.T) { } } } + +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..8266e23f5a 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 @@ -123,6 +124,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 +136,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 +148,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..6730e696ff 100644 --- a/flag_uint.go +++ b/flag_uint.go @@ -86,7 +86,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..4af65fa9e4 100644 --- a/flag_uint64.go +++ b/flag_uint64.go @@ -86,7 +86,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/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)) } }