Skip to content

Commit 4ade007

Browse files
authored
Merge pull request #204 from ipfs/feat/stringsDelim
Added DelimitedStringsOption for enabling delimited strings on the CLI
2 parents edc78ef + c6690cc commit 4ade007

File tree

3 files changed

+228
-22
lines changed

3 files changed

+228
-22
lines changed

Diff for: cli/parse.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func setOpts(kv kv, kvType reflect.Kind, opts cmds.OptMap) error {
100100

101101
if kvType == cmds.Strings {
102102
res, _ := opts[kv.Key].([]string)
103-
opts[kv.Key] = append(res, kv.Value.(string))
103+
opts[kv.Key] = append(res, kv.Value.([]string)...)
104104
} else if _, exists := opts[kv.Key]; !exists {
105105
opts[kv.Key] = kv.Value
106106
} else {

Diff for: cli/parse_test.go

+170-20
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,32 @@ func TestSameWords(t *testing.T) {
7777
test(f, f, true)
7878
}
7979

80+
func testOptionHelper(t *testing.T, cmd *cmds.Command, args string, expectedOpts kvs, expectedWords words, expectErr bool) {
81+
req := &cmds.Request{}
82+
err := parse(req, strings.Split(args, " "), cmd)
83+
if err == nil {
84+
err = req.FillDefaults()
85+
}
86+
if expectErr {
87+
if err == nil {
88+
t.Errorf("Command line '%v' parsing should have failed", args)
89+
}
90+
} else if err != nil {
91+
t.Errorf("Command line '%v' failed to parse: %v", args, err)
92+
} else if !sameWords(req.Arguments, expectedWords) || !sameKVs(kvs(req.Options), expectedOpts) {
93+
t.Errorf("Command line '%v':\n parsed as %v %v\n instead of %v %v",
94+
args, req.Options, req.Arguments, expectedOpts, expectedWords)
95+
}
96+
}
97+
8098
func TestOptionParsing(t *testing.T) {
8199
cmd := &cmds.Command{
82100
Options: []cmds.Option{
83101
cmds.StringOption("string", "s", "a string"),
84102
cmds.StringOption("flag", "alias", "multiple long"),
85103
cmds.BoolOption("bool", "b", "a bool"),
86104
cmds.StringsOption("strings", "r", "strings array"),
105+
cmds.DelimitedStringsOption(",", "delimstrings", "d", "comma delimited string array"),
87106
},
88107
Subcommands: map[string]*cmds.Command{
89108
"test": &cmds.Command{},
@@ -95,30 +114,12 @@ func TestOptionParsing(t *testing.T) {
95114
},
96115
}
97116

98-
testHelper := func(args string, expectedOpts kvs, expectedWords words, expectErr bool) {
99-
req := &cmds.Request{}
100-
err := parse(req, strings.Split(args, " "), cmd)
101-
if err == nil {
102-
err = req.FillDefaults()
103-
}
104-
if expectErr {
105-
if err == nil {
106-
t.Errorf("Command line '%v' parsing should have failed", args)
107-
}
108-
} else if err != nil {
109-
t.Errorf("Command line '%v' failed to parse: %v", args, err)
110-
} else if !sameWords(req.Arguments, expectedWords) || !sameKVs(kvs(req.Options), expectedOpts) {
111-
t.Errorf("Command line '%v':\n parsed as %v %v\n instead of %v %v",
112-
args, req.Options, req.Arguments, expectedOpts, expectedWords)
113-
}
114-
}
115-
116117
testFail := func(args string) {
117-
testHelper(args, kvs{}, words{}, true)
118+
testOptionHelper(t, cmd, args, kvs{}, words{}, true)
118119
}
119120

120121
test := func(args string, expectedOpts kvs, expectedWords words) {
121-
testHelper(args, expectedOpts, expectedWords, false)
122+
testOptionHelper(t, cmd, args, expectedOpts, expectedWords, false)
122123
}
123124

124125
test("test -", kvs{}, words{"-"})
@@ -154,6 +155,13 @@ func TestOptionParsing(t *testing.T) {
154155
test("-b --string foo test bar", kvs{"bool": true, "string": "foo"}, words{"bar"})
155156
test("-b=false --string bar", kvs{"bool": false, "string": "bar"}, words{})
156157
test("--strings a --strings b", kvs{"strings": []string{"a", "b"}}, words{})
158+
159+
test("--delimstrings a,b", kvs{"delimstrings": []string{"a", "b"}}, words{})
160+
test("--delimstrings=a,b", kvs{"delimstrings": []string{"a", "b"}}, words{})
161+
test("-d a,b", kvs{"delimstrings": []string{"a", "b"}}, words{})
162+
test("-d=a,b", kvs{"delimstrings": []string{"a", "b"}}, words{})
163+
test("-d=a,b -d c --delimstrings d", kvs{"delimstrings": []string{"a", "b", "c", "d"}}, words{})
164+
157165
testFail("foo test")
158166
test("defaults", kvs{"opt": "def"}, words{})
159167
test("defaults -o foo", kvs{"opt": "foo"}, words{})
@@ -170,6 +178,148 @@ func TestOptionParsing(t *testing.T) {
170178
testFail("-zz--- --")
171179
}
172180

181+
func TestDefaultOptionParsing(t *testing.T) {
182+
testPanic := func(f func()) {
183+
fnFinished := false
184+
defer func() {
185+
if r := recover(); fnFinished == true {
186+
panic(r)
187+
}
188+
}()
189+
f()
190+
fnFinished = true
191+
t.Error("expected panic")
192+
}
193+
194+
testPanic(func() { cmds.StringOption("string", "s", "a string").WithDefault(0) })
195+
testPanic(func() { cmds.StringOption("string", "s", "a string").WithDefault(false) })
196+
testPanic(func() { cmds.StringOption("string", "s", "a string").WithDefault(nil) })
197+
testPanic(func() { cmds.StringOption("string", "s", "a string").WithDefault([]string{"foo"}) })
198+
testPanic(func() { cmds.StringsOption("strings", "a", "a string array").WithDefault(0) })
199+
testPanic(func() { cmds.StringsOption("strings", "a", "a string array").WithDefault(false) })
200+
testPanic(func() { cmds.StringsOption("strings", "a", "a string array").WithDefault(nil) })
201+
testPanic(func() { cmds.StringsOption("strings", "a", "a string array").WithDefault("foo") })
202+
testPanic(func() { cmds.StringsOption("strings", "a", "a string array").WithDefault([]bool{false}) })
203+
testPanic(func() { cmds.DelimitedStringsOption(",", "dstrings", "d", "delimited string array").WithDefault(0) })
204+
testPanic(func() { cmds.DelimitedStringsOption(",", "dstrs", "d", "delimited string array").WithDefault(false) })
205+
testPanic(func() { cmds.DelimitedStringsOption(",", "dstrings", "d", "delimited string array").WithDefault(nil) })
206+
testPanic(func() { cmds.DelimitedStringsOption(",", "dstrs", "d", "delimited string array").WithDefault("foo") })
207+
testPanic(func() { cmds.DelimitedStringsOption(",", "dstrs", "d", "delimited string array").WithDefault([]int{0}) })
208+
209+
testPanic(func() { cmds.BoolOption("bool", "b", "a bool").WithDefault(0) })
210+
testPanic(func() { cmds.BoolOption("bool", "b", "a bool").WithDefault(1) })
211+
testPanic(func() { cmds.BoolOption("bool", "b", "a bool").WithDefault(nil) })
212+
testPanic(func() { cmds.BoolOption("bool", "b", "a bool").WithDefault([]bool{false}) })
213+
testPanic(func() { cmds.BoolOption("bool", "b", "a bool").WithDefault([]string{"foo"}) })
214+
215+
testPanic(func() { cmds.UintOption("uint", "u", "a uint").WithDefault(int(0)) })
216+
testPanic(func() { cmds.UintOption("uint", "u", "a uint").WithDefault(int32(0)) })
217+
testPanic(func() { cmds.UintOption("uint", "u", "a uint").WithDefault(int64(0)) })
218+
testPanic(func() { cmds.UintOption("uint", "u", "a uint").WithDefault(uint64(0)) })
219+
testPanic(func() { cmds.UintOption("uint", "u", "a uint").WithDefault(uint32(0)) })
220+
testPanic(func() { cmds.UintOption("uint", "u", "a uint").WithDefault(float32(0)) })
221+
testPanic(func() { cmds.UintOption("uint", "u", "a uint").WithDefault(float64(0)) })
222+
testPanic(func() { cmds.UintOption("uint", "u", "a uint").WithDefault(nil) })
223+
testPanic(func() { cmds.UintOption("uint", "u", "a uint").WithDefault([]uint{0}) })
224+
testPanic(func() { cmds.UintOption("uint", "u", "a uint").WithDefault([]string{"foo"}) })
225+
testPanic(func() { cmds.Uint64Option("uint64", "v", "a uint64").WithDefault(int(0)) })
226+
testPanic(func() { cmds.Uint64Option("uint64", "v", "a uint64").WithDefault(int32(0)) })
227+
testPanic(func() { cmds.Uint64Option("uint64", "v", "a uint64").WithDefault(int64(0)) })
228+
testPanic(func() { cmds.Uint64Option("uint64", "v", "a uint64").WithDefault(uint(0)) })
229+
testPanic(func() { cmds.Uint64Option("uint64", "v", "a uint64").WithDefault(uint32(0)) })
230+
testPanic(func() { cmds.Uint64Option("uint64", "v", "a uint64").WithDefault(float32(0)) })
231+
testPanic(func() { cmds.Uint64Option("uint64", "v", "a uint64").WithDefault(float64(0)) })
232+
testPanic(func() { cmds.Uint64Option("uint64", "v", "a uint64").WithDefault(nil) })
233+
testPanic(func() { cmds.Uint64Option("uint64", "v", "a uint64").WithDefault([]uint64{0}) })
234+
testPanic(func() { cmds.Uint64Option("uint64", "v", "a uint64").WithDefault([]string{"foo"}) })
235+
testPanic(func() { cmds.IntOption("int", "i", "an int").WithDefault(int32(0)) })
236+
testPanic(func() { cmds.IntOption("int", "i", "an int").WithDefault(int64(0)) })
237+
testPanic(func() { cmds.IntOption("int", "i", "an int").WithDefault(uint(0)) })
238+
testPanic(func() { cmds.IntOption("int", "i", "an int").WithDefault(uint32(0)) })
239+
testPanic(func() { cmds.IntOption("int", "i", "an int").WithDefault(uint64(0)) })
240+
testPanic(func() { cmds.IntOption("int", "i", "an int").WithDefault(float32(0)) })
241+
testPanic(func() { cmds.IntOption("int", "i", "an int").WithDefault(float64(0)) })
242+
testPanic(func() { cmds.IntOption("int", "i", "an int").WithDefault(nil) })
243+
testPanic(func() { cmds.IntOption("int", "i", "an int").WithDefault([]int{0}) })
244+
testPanic(func() { cmds.IntOption("int", "i", "an int").WithDefault([]string{"foo"}) })
245+
testPanic(func() { cmds.Int64Option("int64", "j", "an int64").WithDefault(int(0)) })
246+
testPanic(func() { cmds.Int64Option("int64", "j", "an int64").WithDefault(int32(0)) })
247+
testPanic(func() { cmds.Int64Option("int64", "j", "an int64").WithDefault(uint(0)) })
248+
testPanic(func() { cmds.Int64Option("int64", "j", "an int64").WithDefault(uint32(0)) })
249+
testPanic(func() { cmds.Int64Option("int64", "j", "an int64").WithDefault(uint64(0)) })
250+
testPanic(func() { cmds.Int64Option("int64", "j", "an int64").WithDefault(float32(0)) })
251+
testPanic(func() { cmds.Int64Option("int64", "j", "an int64").WithDefault(float64(0)) })
252+
testPanic(func() { cmds.Int64Option("int64", "j", "an int64").WithDefault(nil) })
253+
testPanic(func() { cmds.Int64Option("int64", "j", "an int64").WithDefault([]int64{0}) })
254+
testPanic(func() { cmds.Int64Option("int64", "j", "an int64").WithDefault([]string{"foo"}) })
255+
testPanic(func() { cmds.FloatOption("float", "f", "a float64").WithDefault(int(0)) })
256+
testPanic(func() { cmds.FloatOption("float", "f", "a float64").WithDefault(int32(0)) })
257+
testPanic(func() { cmds.FloatOption("float", "f", "a float64").WithDefault(int64(0)) })
258+
testPanic(func() { cmds.FloatOption("float", "f", "a float64").WithDefault(uint(0)) })
259+
testPanic(func() { cmds.FloatOption("float", "f", "a float64").WithDefault(uint32(0)) })
260+
testPanic(func() { cmds.FloatOption("float", "f", "a float64").WithDefault(uint64(0)) })
261+
testPanic(func() { cmds.FloatOption("float", "f", "a float64").WithDefault(float32(0)) })
262+
testPanic(func() { cmds.FloatOption("float", "f", "a float64").WithDefault(nil) })
263+
testPanic(func() { cmds.FloatOption("float", "f", "a float64").WithDefault([]int{0}) })
264+
testPanic(func() { cmds.FloatOption("float", "f", "a float64").WithDefault([]string{"foo"}) })
265+
266+
cmd := &cmds.Command{
267+
Subcommands: map[string]*cmds.Command{
268+
"defaults": &cmds.Command{
269+
Options: []cmds.Option{
270+
cmds.StringOption("string", "s", "a string").WithDefault("foo"),
271+
cmds.StringsOption("strings1", "a", "a string array").WithDefault([]string{"foo"}),
272+
cmds.StringsOption("strings2", "b", "a string array").WithDefault([]string{"foo", "bar"}),
273+
cmds.DelimitedStringsOption(",", "dstrings1", "c", "a delimited string array").WithDefault([]string{"foo"}),
274+
cmds.DelimitedStringsOption(",", "dstrings2", "d", "a delimited string array").WithDefault([]string{"foo", "bar"}),
275+
276+
cmds.BoolOption("boolT", "t", "a bool").WithDefault(true),
277+
cmds.BoolOption("boolF", "a bool").WithDefault(false),
278+
279+
cmds.UintOption("uint", "u", "a uint").WithDefault(uint(1)),
280+
cmds.Uint64Option("uint64", "v", "a uint64").WithDefault(uint64(1)),
281+
cmds.IntOption("int", "i", "an int").WithDefault(int(1)),
282+
cmds.Int64Option("int64", "j", "an int64").WithDefault(int64(1)),
283+
cmds.FloatOption("float", "f", "a float64").WithDefault(float64(1)),
284+
},
285+
},
286+
},
287+
}
288+
289+
test := func(args string, expectedOpts kvs, expectedWords words) {
290+
testOptionHelper(t, cmd, args, expectedOpts, expectedWords, false)
291+
}
292+
293+
test("defaults", kvs{
294+
"string": "foo",
295+
"strings1": []string{"foo"},
296+
"strings2": []string{"foo", "bar"},
297+
"dstrings1": []string{"foo"},
298+
"dstrings2": []string{"foo", "bar"},
299+
"boolT": true,
300+
"boolF": false,
301+
"uint": uint(1),
302+
"uint64": uint64(1),
303+
"int": int(1),
304+
"int64": int64(1),
305+
"float": float64(1),
306+
}, words{})
307+
test("defaults --string baz --strings1=baz -b baz -b=foo -c=foo -d=foo,baz,bing -d=zip,zap -d=zorp -t=false --boolF -u=0 -v=10 -i=-5 -j=10 -f=-3.14", kvs{
308+
"string": "baz",
309+
"strings1": []string{"baz"},
310+
"strings2": []string{"baz", "foo"},
311+
"dstrings1": []string{"foo"},
312+
"dstrings2": []string{"foo", "baz", "bing", "zip", "zap", "zorp"},
313+
"boolT": false,
314+
"boolF": true,
315+
"uint": uint(0),
316+
"uint64": uint64(10),
317+
"int": int(-5),
318+
"int64": int64(10),
319+
"float": float64(-3.14),
320+
}, words{})
321+
}
322+
173323
func TestArgumentParsing(t *testing.T) {
174324
rootCmd := &cmds.Command{
175325
Subcommands: map[string]*cmds.Command{

Diff for: option.go

+57-1
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,18 @@ func NewOption(kind reflect.Kind, names ...string) Option {
149149
}
150150

151151
func (o *option) WithDefault(v interface{}) Option {
152+
if v == nil {
153+
panic(fmt.Errorf("cannot use nil as a default"))
154+
}
155+
156+
// if type of value does not match the option type
157+
if vKind, oKind := reflect.TypeOf(v).Kind(), o.Type(); vKind != oKind {
158+
// if the reason they do not match is not because of Slice vs Array equivalence
159+
// Note: Figuring out if the type of Slice/Array matches is not done in this function
160+
if !((vKind == reflect.Array || vKind == reflect.Slice) && (oKind == reflect.Array || oKind == reflect.Slice)) {
161+
panic(fmt.Errorf("invalid default for the given type, expected %s got %s", o.Type(), vKind))
162+
}
163+
}
152164
o.defaultVal = v
153165
return o
154166
}
@@ -184,6 +196,50 @@ func FloatOption(names ...string) Option {
184196
func StringOption(names ...string) Option {
185197
return NewOption(String, names...)
186198
}
199+
200+
// StringsOption is a command option that can handle a slice of strings
187201
func StringsOption(names ...string) Option {
188-
return NewOption(Strings, names...)
202+
return &stringsOption{
203+
Option: NewOption(Strings, names...),
204+
delimiter: "",
205+
}
206+
}
207+
208+
// DelimitedStringsOption like StringsOption is a command option that can handle a slice of strings.
209+
// However, DelimitedStringsOption will automatically break up the associated CLI inputs based on the delimiter.
210+
// For example, instead of passing `command --option=val1 --option=val2` you can pass `command --option=val1,val2` or
211+
// even `command --option=val1,val2 --option=val3,val4`.
212+
//
213+
// A delimiter of "" is invalid
214+
func DelimitedStringsOption(delimiter string, names ...string) Option {
215+
if delimiter == "" {
216+
panic("cannot create a DelimitedStringsOption with no delimiter")
217+
}
218+
return &stringsOption{
219+
Option: NewOption(Strings, names...),
220+
delimiter: delimiter,
221+
}
222+
}
223+
224+
type stringsOption struct {
225+
Option
226+
delimiter string
227+
}
228+
229+
func (s *stringsOption) WithDefault(v interface{}) Option {
230+
if v == nil {
231+
return s.Option.WithDefault(v)
232+
}
233+
234+
defVal := v.([]string)
235+
s.Option = s.Option.WithDefault(defVal)
236+
return s
237+
}
238+
239+
func (s *stringsOption) Parse(v string) (interface{}, error) {
240+
if s.delimiter == "" {
241+
return []string{v}, nil
242+
}
243+
244+
return strings.Split(v, s.delimiter), nil
189245
}

0 commit comments

Comments
 (0)