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.
This commit is contained in:
Mark Gritter
2020-08-26 14:40:23 -05:00
committed by GitHub
parent c990068679
commit 46bb98b044
2 changed files with 245 additions and 0 deletions

View File

@@ -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 {

115
command/base_flags_test.go Normal file
View File

@@ -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())
}
}
}