From 1992c5a7b88da3490ad7ac088c72dd20b466ee8d Mon Sep 17 00:00:00 2001 From: Maximilian Frank Date: Wed, 23 Mar 2022 12:42:55 +0100 Subject: [PATCH 1/5] Add support for time.Time flags --- time.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++ time_test.go | 65 ++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 time.go create mode 100644 time_test.go diff --git a/time.go b/time.go new file mode 100644 index 00000000..7964346e --- /dev/null +++ b/time.go @@ -0,0 +1,120 @@ +package pflag + +import ( + "fmt" + "strings" + "time" +) + +// TimeValue adapts time.Time for use as a flag. +type TimeValue struct { + *time.Time + formats []string +} + +func newTimeValue(val time.Time, p *time.Time, formats []string) *TimeValue { + *p = val + return &TimeValue{ + Time: p, + formats: formats, + } +} + +// Set time.Time value from string based on accepted formats. +func (d *TimeValue) Set(s string) error { + s = strings.TrimSpace(s) + for _, f := range d.formats { + v, err := time.Parse(f, s) + if err != nil { + continue + } + *d.Time = v + return nil + } + + formatsString := "" + for i, f := range d.formats { + if i > 0 { + formatsString += ", " + } + formatsString += fmt.Sprintf("`%s`", f) + } + + return fmt.Errorf("invalid time format `%s` must be one of: %s", s, formatsString) +} + +// Type name for time.Time flags. +func (d *TimeValue) Type() string { + return "time" +} + +func (d *TimeValue) String() string { return d.Time.Format(time.RFC3339Nano) } + +// GetTime return the time value of a flag with the given name +func (f *FlagSet) GetTime(name string) (time.Time, error) { + flag := f.Lookup(name) + if flag == nil { + err := fmt.Errorf("flag accessed but not defined: %s", name) + return time.Time{}, err + } + + if flag.Value.Type() != "time" { + err := fmt.Errorf("trying to get %s value of flag of type %s", "time", flag.Value.Type()) + return time.Time{}, err + } + + val, ok := flag.Value.(*TimeValue) + if !ok { + return time.Time{}, fmt.Errorf("value %s is not a time", flag.Value) + } + + return *val.Time, nil +} + +// TimeVar defines a time.Time flag with specified name, default value, and usage string. +// The argument p points to a time.Time variable in which to store the value of the flag. +func (f *FlagSet) TimeVar(p *time.Time, name string, value time.Time, formats []string, usage string) { + f.VarP(newTimeValue(value, p, formats), name, "", usage) +} + +// TimeVarP is like TimeVar, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) TimeVarP(p *time.Time, name, shorthand string, value time.Time, formats []string, usage string) { + f.VarP(newTimeValue(value, p, formats), name, shorthand, usage) +} + +// TimeVar defines a time.Time flag with specified name, default value, and usage string. +// The argument p points to a time.Time variable in which to store the value of the flag. +func TimeVar(p *time.Time, name string, value time.Time, formats []string, usage string) { + CommandLine.VarP(newTimeValue(value, p, formats), name, "", usage) +} + +// TimeVarP is like TimeVar, but accepts a shorthand letter that can be used after a single dash. +func TimeVarP(p *time.Time, name, shorthand string, value time.Time, formats []string, usage string) { + CommandLine.VarP(newTimeValue(value, p, formats), name, shorthand, usage) +} + +// Time defines a time.Time flag with specified name, default value, and usage string. +// The return value is the address of a time.Time variable that stores the value of the flag. +func (f *FlagSet) Time(name string, value time.Time, formats []string, usage string) *time.Time { + p := new(time.Time) + f.TimeVarP(p, name, "", value, formats, usage) + return p +} + +// TimeP is like Time, but accepts a shorthand letter that can be used after a single dash. +func (f *FlagSet) TimeP(name, shorthand string, value time.Time, formats []string, usage string) *time.Time { + p := new(time.Time) + f.TimeVarP(p, name, shorthand, value, formats, usage) + return p +} + +// Time defines a time.Time flag with specified name, default value, and usage string. +// The return value is the address of a time.Time variable that stores the value of the flag. +func Time(name string, value time.Time, formats []string, usage string) *time.Time { + return CommandLine.TimeP(name, "", value, formats, usage) +} + +// TimeP is like Time, but accepts a shorthand letter that can be used after a single dash. +func TimeP(name, shorthand string, value time.Time, formats []string, usage string) *time.Time { + return CommandLine.TimeP(name, shorthand, value, formats, usage) +} diff --git a/time_test.go b/time_test.go new file mode 100644 index 00000000..3cb10096 --- /dev/null +++ b/time_test.go @@ -0,0 +1,65 @@ +package pflag + +import ( + "fmt" + "os" + "testing" + "time" +) + +func setUpTimeVar(t *time.Time, formats []string) *FlagSet { + f := NewFlagSet("test", ContinueOnError) + f.TimeVar(t, "time", time.Time{}, formats, "Time") + return f +} + +func TestTime(t *testing.T) { + testCases := []struct { + input string + success bool + expected string + }{ + {"2022-01-01T01:01:01+00:00", true, "2022-01-01T01:01:01Z"}, + {" 2022-01-01T01:01:01+00:00", true, "2022-01-01T01:01:01Z"}, + {"2022-01-01T01:01:01+00:00 ", true, "2022-01-01T01:01:01Z"}, + {"2022-01-01T01:01:01+02:00", true, "2022-01-01T01:01:01+02:00"}, + {"2022-01-01T01:01:01.01+02:00", true, "2022-01-01T01:01:01.01+02:00"}, + {"Sat, 01 Jan 2022 01:01:01 +0000", true, "2022-01-01T01:01:01Z"}, + {"Sat, 01 Jan 2022 01:01:01 +0200", true, "2022-01-01T01:01:01+02:00"}, + {"Sat, 01 Jan 2022 01:01:01 +0000", true, "2022-01-01T01:01:01Z"}, + {"", false, ""}, + {"not a date", false, ""}, + {"2022-01-01 01:01:01", false, ""}, + {"2022-01-01T01:01:01", false, ""}, + {"01 Jan 2022 01:01:01 +0000", false, ""}, + {"Sat, 01 Jan 2022 01:01:01", false, ""}, + } + + devnull, _ := os.Open(os.DevNull) + os.Stderr = devnull + for i := range testCases { + var timeVar time.Time + formats := []string{time.RFC3339Nano, time.RFC1123Z} + f := setUpTimeVar(&timeVar, formats) + + tc := &testCases[i] + + arg := fmt.Sprintf("--time=%s", tc.input) + err := f.Parse([]string{arg}) + if err != nil && tc.success == true { + t.Errorf("expected success, got %q", err) + continue + } else if err == nil && tc.success == false { + t.Errorf("expected failure") + continue + } else if tc.success { + timeResult, err := f.GetTime("time") + if err != nil { + t.Errorf("Got error trying to fetch the Time flag: %v", err) + } + if timeResult.Format(time.RFC3339Nano) != tc.expected { + t.Errorf("expected %q, got %q", tc.expected, timeVar.Format(time.RFC3339Nano)) + } + } + } +} From c5ce22e836c6268eb270e8f28ae5e3729a27c82d Mon Sep 17 00:00:00 2001 From: Maximilian Frank Date: Mon, 14 Jul 2025 08:44:29 +0900 Subject: [PATCH 2/5] Use time.Time for expectations in time flag tests --- time_test.go | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/time_test.go b/time_test.go index 3cb10096..c0029102 100644 --- a/time_test.go +++ b/time_test.go @@ -17,22 +17,22 @@ func TestTime(t *testing.T) { testCases := []struct { input string success bool - expected string + expected time.Time }{ - {"2022-01-01T01:01:01+00:00", true, "2022-01-01T01:01:01Z"}, - {" 2022-01-01T01:01:01+00:00", true, "2022-01-01T01:01:01Z"}, - {"2022-01-01T01:01:01+00:00 ", true, "2022-01-01T01:01:01Z"}, - {"2022-01-01T01:01:01+02:00", true, "2022-01-01T01:01:01+02:00"}, - {"2022-01-01T01:01:01.01+02:00", true, "2022-01-01T01:01:01.01+02:00"}, - {"Sat, 01 Jan 2022 01:01:01 +0000", true, "2022-01-01T01:01:01Z"}, - {"Sat, 01 Jan 2022 01:01:01 +0200", true, "2022-01-01T01:01:01+02:00"}, - {"Sat, 01 Jan 2022 01:01:01 +0000", true, "2022-01-01T01:01:01Z"}, - {"", false, ""}, - {"not a date", false, ""}, - {"2022-01-01 01:01:01", false, ""}, - {"2022-01-01T01:01:01", false, ""}, - {"01 Jan 2022 01:01:01 +0000", false, ""}, - {"Sat, 01 Jan 2022 01:01:01", false, ""}, + {"2022-01-01T01:01:01+00:00", true, time.Date(2022, 1, 1, 1, 1, 1, 0, time.UTC)}, + {" 2022-01-01T01:01:01+00:00", true, time.Date(2022, 1, 1, 1, 1, 1, 0, time.UTC)}, + {"2022-01-01T01:01:01+00:00 ", true, time.Date(2022, 1, 1, 1, 1, 1, 0, time.UTC)}, + {"2022-01-01T01:01:01+02:00", true, time.Date(2022, 1, 1, 1, 1, 1, 0, time.FixedZone("UTC+2", 2*60*60))}, + {"2022-01-01T01:01:01.01+02:00", true, time.Date(2022, 1, 1, 1, 1, 1, 10000000, time.FixedZone("UTC+2", 2*60*60))}, + {"Sat, 01 Jan 2022 01:01:01 +0000", true, time.Date(2022, 1, 1, 1, 1, 1, 0, time.UTC)}, + {"Sat, 01 Jan 2022 01:01:01 +0200", true, time.Date(2022, 1, 1, 1, 1, 1, 0, time.FixedZone("UTC+2", 2*60*60))}, + {"Sat, 01 Jan 2022 01:01:01 +0000", true, time.Date(2022, 1, 1, 1, 1, 1, 0, time.UTC)}, + {"", false, time.Time{}}, + {"not a date", false, time.Time{}}, + {"2022-01-01 01:01:01", false, time.Time{}}, + {"2022-01-01T01:01:01", false, time.Time{}}, + {"01 Jan 2022 01:01:01 +0000", false, time.Time{}}, + {"Sat, 01 Jan 2022 01:01:01", false, time.Time{}}, } devnull, _ := os.Open(os.DevNull) @@ -57,8 +57,8 @@ func TestTime(t *testing.T) { if err != nil { t.Errorf("Got error trying to fetch the Time flag: %v", err) } - if timeResult.Format(time.RFC3339Nano) != tc.expected { - t.Errorf("expected %q, got %q", tc.expected, timeVar.Format(time.RFC3339Nano)) + if !timeResult.Equal(tc.expected) { + t.Errorf("expected %q, got %q", tc.expected.Format(time.RFC3339Nano), timeVar.Format(time.RFC3339Nano)) } } } From d15848db482b52179577da9738cf9702d8d28466 Mon Sep 17 00:00:00 2001 From: Maximilian Frank Date: Mon, 14 Jul 2025 08:52:47 +0900 Subject: [PATCH 3/5] Remove unnecessary time test stderr dev null redirect --- time_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/time_test.go b/time_test.go index c0029102..46a5ada7 100644 --- a/time_test.go +++ b/time_test.go @@ -2,7 +2,6 @@ package pflag import ( "fmt" - "os" "testing" "time" ) @@ -35,8 +34,6 @@ func TestTime(t *testing.T) { {"Sat, 01 Jan 2022 01:01:01", false, time.Time{}}, } - devnull, _ := os.Open(os.DevNull) - os.Stderr = devnull for i := range testCases { var timeVar time.Time formats := []string{time.RFC3339Nano, time.RFC1123Z} From 7cc25e3bdd8c540b243f70c366ba1f1856fcd9e9 Mon Sep 17 00:00:00 2001 From: Tomas Aschan <1550920+tomasaschan@users.noreply.github.com> Date: Wed, 16 Jul 2025 23:37:38 +0200 Subject: [PATCH 4/5] Don't export `TimeValue` (yet) --- time.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/time.go b/time.go index 7964346e..dbaeeaba 100644 --- a/time.go +++ b/time.go @@ -7,21 +7,21 @@ import ( ) // TimeValue adapts time.Time for use as a flag. -type TimeValue struct { +type timeValue struct { *time.Time formats []string } -func newTimeValue(val time.Time, p *time.Time, formats []string) *TimeValue { +func newTimeValue(val time.Time, p *time.Time, formats []string) *timeValue { *p = val - return &TimeValue{ + return &timeValue{ Time: p, formats: formats, } } // Set time.Time value from string based on accepted formats. -func (d *TimeValue) Set(s string) error { +func (d *timeValue) Set(s string) error { s = strings.TrimSpace(s) for _, f := range d.formats { v, err := time.Parse(f, s) @@ -44,11 +44,11 @@ func (d *TimeValue) Set(s string) error { } // Type name for time.Time flags. -func (d *TimeValue) Type() string { +func (d *timeValue) Type() string { return "time" } -func (d *TimeValue) String() string { return d.Time.Format(time.RFC3339Nano) } +func (d *timeValue) String() string { return d.Time.Format(time.RFC3339Nano) } // GetTime return the time value of a flag with the given name func (f *FlagSet) GetTime(name string) (time.Time, error) { @@ -63,7 +63,7 @@ func (f *FlagSet) GetTime(name string) (time.Time, error) { return time.Time{}, err } - val, ok := flag.Value.(*TimeValue) + val, ok := flag.Value.(*timeValue) if !ok { return time.Time{}, fmt.Errorf("value %s is not a time", flag.Value) } From e3be2ebcffcc36be35e23d418d3e0ba86239826a Mon Sep 17 00:00:00 2001 From: Tomas Aschan <1550920+tomasaschan@users.noreply.github.com> Date: Wed, 16 Jul 2025 23:38:11 +0200 Subject: [PATCH 5/5] Reduce duplication by forwarding to sibling functions --- time.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/time.go b/time.go index dbaeeaba..dc024807 100644 --- a/time.go +++ b/time.go @@ -74,7 +74,7 @@ func (f *FlagSet) GetTime(name string) (time.Time, error) { // TimeVar defines a time.Time flag with specified name, default value, and usage string. // The argument p points to a time.Time variable in which to store the value of the flag. func (f *FlagSet) TimeVar(p *time.Time, name string, value time.Time, formats []string, usage string) { - f.VarP(newTimeValue(value, p, formats), name, "", usage) + f.TimeVarP(p, name, "", value, formats, usage) } // TimeVarP is like TimeVar, but accepts a shorthand letter that can be used after a single dash. @@ -85,7 +85,7 @@ func (f *FlagSet) TimeVarP(p *time.Time, name, shorthand string, value time.Time // TimeVar defines a time.Time flag with specified name, default value, and usage string. // The argument p points to a time.Time variable in which to store the value of the flag. func TimeVar(p *time.Time, name string, value time.Time, formats []string, usage string) { - CommandLine.VarP(newTimeValue(value, p, formats), name, "", usage) + CommandLine.TimeVarP(p, name, "", value, formats, usage) } // TimeVarP is like TimeVar, but accepts a shorthand letter that can be used after a single dash. @@ -96,9 +96,7 @@ func TimeVarP(p *time.Time, name, shorthand string, value time.Time, formats []s // Time defines a time.Time flag with specified name, default value, and usage string. // The return value is the address of a time.Time variable that stores the value of the flag. func (f *FlagSet) Time(name string, value time.Time, formats []string, usage string) *time.Time { - p := new(time.Time) - f.TimeVarP(p, name, "", value, formats, usage) - return p + return f.TimeP(name, "", value, formats, usage) } // TimeP is like Time, but accepts a shorthand letter that can be used after a single dash.