From 46bb98b0440f0b09b15786d61ec565df7aa2ee11 Mon Sep 17 00:00:00 2001 From: Mark Gritter Date: Wed, 26 Aug 2020 14:40:23 -0500 Subject: [PATCH] Add date/time argument type. (#9817) * Add date/time argument type. * Add an argument to select which time formats are valid. * Increase minimum date for epoch timestamps to avoid ambiguity. --- command/base_flags.go | 130 +++++++++++++++++++++++++++++++++++++ command/base_flags_test.go | 115 ++++++++++++++++++++++++++++++++ 2 files changed, 245 insertions(+) create mode 100644 command/base_flags_test.go diff --git a/command/base_flags.go b/command/base_flags.go index 0cdb01f8d3..39202857bf 100644 --- a/command/base_flags.go +++ b/command/base_flags.go @@ -1,6 +1,7 @@ package command import ( + "errors" "flag" "fmt" "os" @@ -749,6 +750,135 @@ func (f *FlagSet) Var(value flag.Value, name, usage string) { f.flagSet.Var(value, name, usage) } +// -- TimeVar and timeValue +type TimeVar struct { + Name string + Aliases []string + Usage string + Default time.Time + Hidden bool + EnvVar string + Target *time.Time + Completion complete.Predictor + Formats TimeFormat +} + +// Identify the allowable formats, identified by the minimum +// precision accepted. +// TODO: move this somewhere where it can be re-used for the API. +type TimeFormat int + +const ( + TimeVar_EpochSecond TimeFormat = 1 << iota + TimeVar_RFC3339Nano + TimeVar_RFC3339Second + TimeVar_Day + TimeVar_Month +) + +// Default value to use +const TimeVar_TimeOrDay TimeFormat = TimeVar_EpochSecond | TimeVar_RFC3339Nano | TimeVar_RFC3339Second | TimeVar_Day + +// parseTimeAlternatives attempts several different allowable variants +// of the time field. +func parseTimeAlternatives(input string, allowedFormats TimeFormat) (time.Time, error) { + // The RFC3339 formats require the inclusion of a time zone. + if allowedFormats&TimeVar_RFC3339Nano != 0 { + t, err := time.Parse(time.RFC3339Nano, input) + if err == nil { + return t, err + } + } + + if allowedFormats&TimeVar_RFC3339Second != 0 { + t, err := time.Parse(time.RFC3339, input) + if err == nil { + return t, err + } + } + + if allowedFormats&TimeVar_Day != 0 { + t, err := time.Parse("2006-01-02", input) + if err == nil { + return t, err + } + } + + if allowedFormats&TimeVar_Month != 0 { + t, err := time.Parse("2006-01", input) + if err == nil { + return t, err + } + } + + if allowedFormats&TimeVar_EpochSecond != 0 { + i, err := strconv.ParseInt(input, 10, 64) + if err == nil { + // If a customer enters 20200101 we don't want + // to parse that as an epoch time. + // This arbitrarily-chosen cutoff is around year 2000. + if i > 946000000 { + return time.Unix(i, 0), nil + } + } + } + + return time.Time{}, errors.New("Could not parse as absolute time.") +} + +func (f *FlagSet) TimeVar(i *TimeVar) { + initial := i.Default + if v, exist := os.LookupEnv(i.EnvVar); exist { + if d, err := parseTimeAlternatives(v, i.Formats); err == nil { + initial = d + } + } + + def := "" + if !i.Default.IsZero() { + def = i.Default.String() + } + + f.VarFlag(&VarFlag{ + Name: i.Name, + Aliases: i.Aliases, + Usage: i.Usage, + Default: def, + EnvVar: i.EnvVar, + Value: newTimeValue(initial, i.Target, i.Hidden, i.Formats), + Completion: i.Completion, + }) +} + +type timeValue struct { + hidden bool + target *time.Time + formats TimeFormat +} + +func newTimeValue(def time.Time, target *time.Time, hidden bool, f TimeFormat) *timeValue { + *target = def + return &timeValue{ + hidden: hidden, + target: target, + formats: f, + } +} + +func (d *timeValue) Set(s string) error { + v, err := parseTimeAlternatives(s, d.formats) + if err != nil { + return err + } + *d.target = v + return nil +} + +func (d *timeValue) Get() interface{} { return *d.target } +func (d *timeValue) String() string { return (*d.target).String() } +func (d *timeValue) Example() string { return "time" } +func (d *timeValue) Hidden() bool { return d.hidden } + // -- helpers func envDefault(key, def string) string { if v, exist := os.LookupEnv(key); exist { diff --git a/command/base_flags_test.go b/command/base_flags_test.go new file mode 100644 index 0000000000..eab02f852e --- /dev/null +++ b/command/base_flags_test.go @@ -0,0 +1,115 @@ +package command + +import ( + "testing" + "time" +) + +func Test_TimeParsing(t *testing.T) { + var zeroTime time.Time + + testCases := []struct { + Input string + Formats TimeFormat + Valid bool + Expected time.Time + }{ + { + "2020-08-24", + TimeVar_TimeOrDay, + true, + time.Date(2020, 8, 24, 0, 0, 0, 0, time.UTC), + }, + { + "2099-09", + TimeVar_TimeOrDay, + false, + zeroTime, + }, + { + "2099-09", + TimeVar_TimeOrDay | TimeVar_Month, + true, + time.Date(2099, 9, 1, 0, 0, 0, 0, time.UTC), + }, + { + "2021-01-02T03:04:05-02:00", + TimeVar_TimeOrDay, + true, + time.Date(2021, 1, 2, 5, 4, 5, 0, time.UTC), + }, + { + "2021-01-02T03:04:05", + TimeVar_TimeOrDay, + false, // Missing timezone not supported + time.Date(2021, 1, 2, 3, 4, 5, 0, time.UTC), + }, + { + "2021-01-02T03:04:05+02:00", + TimeVar_TimeOrDay, + true, + time.Date(2021, 1, 2, 1, 4, 5, 0, time.UTC), + }, + { + "1598313593", + TimeVar_TimeOrDay, + true, + time.Date(2020, 8, 24, 23, 59, 53, 0, time.UTC), + }, + { + "2037", + TimeVar_TimeOrDay, + false, + zeroTime, + }, + { + "20201212", + TimeVar_TimeOrDay, + false, + zeroTime, + }, + { + "9999999999999999999999999999999999999999999999", + TimeVar_TimeOrDay, + false, + zeroTime, + }, + { + "2021-13-02T03:04:05-02:00", + TimeVar_TimeOrDay, + false, + zeroTime, + }, + { + "2021-12-02T24:04:05+00:00", + TimeVar_TimeOrDay, + false, + zeroTime, + }, + { + "2021-01-02T03:04:05.234567890Z", + TimeVar_TimeOrDay, + true, + time.Date(2021, 1, 2, 3, 4, 5, 234567890, time.UTC), + }, + } + + for _, tc := range testCases { + var result time.Time + timeVal := newTimeValue(zeroTime, &result, false, tc.Formats) + err := timeVal.Set(tc.Input) + if err == nil && !tc.Valid { + t.Errorf("Time %q parsed without error as %v, but is not valid", tc.Input, result) + continue + } + if err != nil { + if tc.Valid { + t.Errorf("Time %q parsed as error, but is valid", tc.Input) + } + continue + } + if !tc.Expected.Equal(result) { + t.Errorf("Time %q parsed incorrectly, expected %v but got %v", tc.Input, tc.Expected.UTC(), result.UTC()) + } + } +}