Skip to content

Commit 88c33c4

Browse files
committed
feat: add TextFlag
* Added `TextFlag` which supports setting values for types that satisfies the `encoding.TextMarshaller` and `encoding.TextUnmarshaller` which is very handy when you want to set log levels or string-like types that satifies the interfaces. Fixes: #2051 Signed-off-by: Tobias Dahlberg <[email protected]>
1 parent e8c32ad commit 88c33c4

File tree

4 files changed

+267
-0
lines changed

4 files changed

+267
-0
lines changed

flag_text.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package cli
2+
3+
import (
4+
"encoding"
5+
)
6+
7+
type TextMarshalUnmarshaler interface {
8+
encoding.TextMarshaler
9+
encoding.TextUnmarshaler
10+
}
11+
12+
// TextFlag enables you to set types that satisfies [TextMarshalUnmarshaler] using flags such as log levels.
13+
type TextFlag = FlagBase[TextMarshalUnmarshaler, NoConfig, TextValue]
14+
15+
type TextValue struct {
16+
Value TextMarshalUnmarshaler
17+
}
18+
19+
func (f TextValue) String() string {
20+
text, err := f.Value.MarshalText()
21+
if err != nil {
22+
return ""
23+
}
24+
25+
return string(text)
26+
}
27+
28+
func (f TextValue) Set(s string) error {
29+
return f.Value.UnmarshalText([]byte(s))
30+
}
31+
32+
func (f TextValue) Get() any {
33+
return f.Value
34+
}
35+
36+
func (f TextValue) Create(v TextMarshalUnmarshaler, p *TextMarshalUnmarshaler, _ NoConfig) Value {
37+
pp := *p
38+
if v != nil {
39+
if b, err := v.MarshalText(); err == nil {
40+
_ = pp.UnmarshalText(b)
41+
}
42+
}
43+
44+
return &TextValue{
45+
Value: pp,
46+
}
47+
}
48+
49+
func (f TextValue) ToString(v TextMarshalUnmarshaler) string {
50+
text, _ := v.MarshalText()
51+
52+
return string(text)
53+
}

flag_text_test.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package cli
2+
3+
import (
4+
"encoding"
5+
"errors"
6+
"io"
7+
"log/slog"
8+
"slices"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
type badMarshaller struct{}
16+
17+
func (badMarshaller) UnmarshalText(_ []byte) error {
18+
return nil
19+
}
20+
21+
func (badMarshaller) MarshalText() ([]byte, error) {
22+
return nil, errors.New("bad")
23+
}
24+
25+
func ptr[T any](v T) *T {
26+
return &v
27+
}
28+
29+
func TestTextFlag(t *testing.T) {
30+
tests := []struct {
31+
name string
32+
flag *TextFlag
33+
args []string
34+
want string
35+
wantErr bool
36+
}{
37+
{
38+
name: "empty",
39+
flag: &TextFlag{
40+
Name: "log-level",
41+
Value: &slog.LevelVar{},
42+
Destination: ptr[TextMarshalUnmarshaler](&slog.LevelVar{}),
43+
},
44+
want: "INFO",
45+
},
46+
{
47+
name: "info",
48+
flag: &TextFlag{
49+
Name: "log-level",
50+
Value: &slog.LevelVar{},
51+
Destination: ptr[TextMarshalUnmarshaler](&slog.LevelVar{}),
52+
Validator: func(v TextMarshalUnmarshaler) error {
53+
text, err := v.MarshalText()
54+
if err != nil {
55+
return err
56+
}
57+
58+
if !slices.Equal(text, []byte("INFO")) {
59+
return errors.New("expected \"INFO\"")
60+
}
61+
62+
return nil
63+
},
64+
},
65+
args: []string{"--log-level", "info"},
66+
want: "INFO",
67+
},
68+
{
69+
name: "debug",
70+
flag: &TextFlag{
71+
Name: "log-level",
72+
Value: &slog.LevelVar{},
73+
Destination: ptr[TextMarshalUnmarshaler](&slog.LevelVar{}),
74+
},
75+
args: []string{"--log-level", "debug"},
76+
want: "DEBUG",
77+
},
78+
{
79+
name: "invalid",
80+
flag: &TextFlag{
81+
Name: "log-level",
82+
Value: &slog.LevelVar{},
83+
Destination: ptr[TextMarshalUnmarshaler](&slog.LevelVar{}),
84+
},
85+
args: []string{"--log-level", "invalid"},
86+
want: "INFO",
87+
wantErr: true,
88+
},
89+
{
90+
name: "bad_marshaller",
91+
flag: &TextFlag{
92+
Name: "text",
93+
Value: &badMarshaller{},
94+
Destination: ptr[TextMarshalUnmarshaler](&badMarshaller{}),
95+
},
96+
args: []string{"--text", "foo"},
97+
wantErr: true,
98+
},
99+
{
100+
name: "default",
101+
flag: &TextFlag{
102+
Name: "log-level",
103+
Value: func() *slog.LevelVar {
104+
var l slog.LevelVar
105+
106+
l.Set(slog.LevelWarn)
107+
108+
return &l
109+
}(),
110+
Destination: ptr[TextMarshalUnmarshaler](&slog.LevelVar{}),
111+
},
112+
args: []string{},
113+
want: "WARN",
114+
},
115+
{
116+
name: "override_default",
117+
flag: &TextFlag{
118+
Name: "log-level",
119+
Value: func() *slog.LevelVar {
120+
var l slog.LevelVar
121+
122+
l.Set(slog.LevelWarn)
123+
124+
return &l
125+
}(),
126+
Destination: ptr[TextMarshalUnmarshaler](&slog.LevelVar{}),
127+
},
128+
args: []string{"--log-level", "error"},
129+
want: "ERROR",
130+
},
131+
}
132+
133+
t.Parallel()
134+
135+
for _, tt := range tests {
136+
t.Parallel()
137+
138+
t.Run(tt.name, func(t *testing.T) {
139+
cmd := &Command{
140+
Name: tt.name,
141+
Flags: []Flag{tt.flag},
142+
Writer: io.Discard,
143+
ErrWriter: io.Discard,
144+
}
145+
146+
err := cmd.Run(buildTestContext(t), append([]string{"mock"}, tt.args...))
147+
148+
if err != nil && !tt.wantErr {
149+
require.NoError(t, err)
150+
151+
return
152+
} else if err != nil {
153+
return
154+
}
155+
156+
var got []byte
157+
158+
got, err = tt.flag.Get().(encoding.TextMarshaler).MarshalText()
159+
if tt.wantErr {
160+
require.Error(t, err)
161+
162+
return
163+
}
164+
165+
assert.Equal(t, tt.want, string(got))
166+
})
167+
}
168+
}

godoc-current.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,29 @@ type SuggestCommandFunc func(commands []*Command, provided string) string
11401140

11411141
type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
11421142

1143+
type TextFlag = FlagBase[TextMarshalUnmarshaler, NoConfig, TextValue]
1144+
TextFlag enables you to set types that satisfies TextMarshalUnmarshaler
1145+
using flags such as log levels.
1146+
1147+
type TextMarshalUnmarshaler interface {
1148+
encoding.TextMarshaler
1149+
encoding.TextUnmarshaler
1150+
}
1151+
1152+
type TextValue struct {
1153+
Value TextMarshalUnmarshaler
1154+
}
1155+
1156+
func (f TextValue) Create(v TextMarshalUnmarshaler, p *TextMarshalUnmarshaler, _ NoConfig) Value
1157+
1158+
func (f TextValue) Get() any
1159+
1160+
func (f TextValue) Set(s string) error
1161+
1162+
func (f TextValue) String() string
1163+
1164+
func (f TextValue) ToString(v TextMarshalUnmarshaler) string
1165+
11431166
type TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue]
11441167

11451168
type TimestampConfig struct {

testdata/godoc-v3.x.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,29 @@ type SuggestCommandFunc func(commands []*Command, provided string) string
11401140

11411141
type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
11421142

1143+
type TextFlag = FlagBase[TextMarshalUnmarshaler, NoConfig, TextValue]
1144+
TextFlag enables you to set types that satisfies TextMarshalUnmarshaler
1145+
using flags such as log levels.
1146+
1147+
type TextMarshalUnmarshaler interface {
1148+
encoding.TextMarshaler
1149+
encoding.TextUnmarshaler
1150+
}
1151+
1152+
type TextValue struct {
1153+
Value TextMarshalUnmarshaler
1154+
}
1155+
1156+
func (f TextValue) Create(v TextMarshalUnmarshaler, p *TextMarshalUnmarshaler, _ NoConfig) Value
1157+
1158+
func (f TextValue) Get() any
1159+
1160+
func (f TextValue) Set(s string) error
1161+
1162+
func (f TextValue) String() string
1163+
1164+
func (f TextValue) ToString(v TextMarshalUnmarshaler) string
1165+
11431166
type TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue]
11441167

11451168
type TimestampConfig struct {

0 commit comments

Comments
 (0)