Skip to content

Commit 8670f35

Browse files
committed
refactor: simplify api surface area by combining parsed opts and args
1 parent e361e11 commit 8670f35

File tree

5 files changed

+141
-192
lines changed

5 files changed

+141
-192
lines changed

builder.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package cli
22

3-
import "strings"
3+
import (
4+
"slices"
5+
"strings"
6+
)
47

58
var DefaultHelpInput = NewBoolOpt("help").
69
Short('h').
@@ -30,15 +33,28 @@ func (c *CommandInfo) prepareAndValidate() {
3033
*c = (*c).Opt(DefaultHelpInput)
3134
}
3235

36+
// assert there are no duplicate input ids across the options and positional arguments
37+
inputIDs := make([]string, 0, len(c.Opts)+len(c.Args))
38+
for i := range len(c.Opts) {
39+
id := c.Opts[i].ID
40+
if slices.Contains(inputIDs, id) {
41+
panic("command '" + strings.Join(c.Path, " ") +
42+
"' contains duplicate input ids '" + id + "'")
43+
}
44+
inputIDs = append(inputIDs, id)
45+
}
46+
for i := range len(c.Args) {
47+
id := c.Args[i].ID
48+
if slices.Contains(inputIDs, id) {
49+
panic("command '" + strings.Join(c.Path, " ") +
50+
"' contains duplicate input ids '" + id + "'")
51+
}
52+
inputIDs = append(inputIDs, id)
53+
}
54+
3355
// option assertions
3456
for i := 0; i < len(c.Opts)-1; i++ {
3557
for z := i + 1; z < len(c.Opts); z++ {
36-
// assert there are no duplicate input ids
37-
if c.Opts[i].ID == c.Opts[z].ID {
38-
panic("command '" + strings.Join(c.Path, " ") +
39-
"' contains duplicate option ids '" + c.Opts[i].ID + "'")
40-
}
41-
4258
// assert there are no duplicate long or short option names
4359
if c.Opts[i].NameShort != "" && c.Opts[i].NameShort == c.Opts[z].NameShort {
4460
panic("command '" + strings.Join(c.Path, " ") +
@@ -51,16 +67,6 @@ func (c *CommandInfo) prepareAndValidate() {
5167
}
5268
}
5369

54-
// assert there are no duplicate arg ids
55-
for i := 0; i < len(c.Args)-1; i++ {
56-
for z := i + 1; z < len(c.Args); z++ {
57-
if c.Args[i].ID == c.Args[z].ID {
58-
panic("command '" + strings.Join(c.Path, " ") +
59-
"' contains duplicate argument ids '" + c.Args[i].ID + "'")
60-
}
61-
}
62-
}
63-
6470
// subcommand names must be unique across Subcmds
6571
for i := 0; i < len(c.Subcmds)-1; i++ {
6672
for z := i + 1; z < len(c.Subcmds); z++ {

builder_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ func TestBuilder(t *testing.T) {
104104
},
105105
},
106106
expPanicVals: []any{
107-
"command 'root one' contains duplicate option ids 'o1'",
108-
"command 'root' contains duplicate argument ids 'a1'",
107+
"command 'root one' contains duplicate input ids 'o1'",
108+
"command 'root' contains duplicate input ids 'a1'",
109109
},
110110
},
111111
{

cli.go

Lines changed: 38 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,7 @@ type HelpGenerator = func(Input, *CommandInfo) string
130130

131131
type Command struct {
132132
Name string
133-
Opts []Input
134-
Args []Input
133+
Inputs []Input
135134
Surplus []string
136135
Subcmd *Command
137136
}
@@ -150,77 +149,39 @@ type ParsedFrom struct {
150149
Default bool
151150
}
152151

153-
// LookupOpt looks for an option value with the given id in the given Command and converts
154-
// the value to the given type T through an untested type assertion (so this will panic if
155-
// the value is found and can't be converted to type T). So if the option is present, the
156-
// typed value will be returned and the boolean will be true. Otherwise, the zero value of
157-
// type T will be returned and the boolean will be false.
158-
func LookupOpt[T any](c *Command, id string) (T, bool) {
159-
for i := len(c.Opts) - 1; i >= 0; i-- {
160-
if c.Opts[i].ID == id {
161-
return c.Opts[i].Value.(T), true
152+
// Lookup looks for a parsed input value with the given id in the given Command and
153+
// converts the value to the given type T through an untested type assertion (so this
154+
// will panic if the value is found and can't be converted to type T). So if the input
155+
// is present, the typed value will be returned and the boolean will be true. Otherwise,
156+
// the zero value of type T will be returned and the boolean will be false.
157+
func Lookup[T any](c *Command, id string) (T, bool) {
158+
for i := len(c.Inputs) - 1; i >= 0; i-- {
159+
if c.Inputs[i].ID == id {
160+
return c.Inputs[i].Value.(T), true
162161
}
163162
}
164163
var zero T
165164
return zero, false
166165
}
167166

168-
// GetOpt gets the option value with the given id in the given Command and converts the
169-
// value to the given type T through an untested type assertion. This function will panic
170-
// if the value isn't found or if the value can't be converted to type T. To check
171-
// whether the value is found instead of panicking, see [LookupOpt].
172-
func GetOpt[T any](c *Command, id string) T {
173-
if v, ok := LookupOpt[T](c, id); ok {
167+
// Get gets the parsed input value with the given id in the given Command and converts
168+
// the value to the given type T through an untested type assertion. This function will
169+
// panic if the value isn't found or if the value can't be converted to type T.
170+
// To check whether the value is found instead of panicking, see [Lookup].
171+
func Get[T any](c *Command, id string) T {
172+
if v, ok := Lookup[T](c, id); ok {
174173
return v
175174
}
176-
panic("no parsed option value found for id '" + id + "'")
175+
panic("no parsed input value for id '" + id + "'")
177176
}
178177

179-
// GetOptOr looks for an option value with the given id in the given Command and converts
180-
// the value to the given type T through an untested type assertion (so this will panic if
181-
// the value is found and can't be converted to type T). If the value isn't found, the
182-
// given fallback value will be returned. To check whether the value is found instead of
183-
// using a fallback value, see [LookupOpt].
184-
func GetOptOr[T any](c *Command, id string, fallback T) T {
185-
if v, ok := LookupOpt[T](c, id); ok {
186-
return v
187-
}
188-
return fallback
189-
}
190-
191-
// LookupArg looks for a positional argument value with the given id in the given Command
192-
// and converts the value to the given type T through an untested type assertion (so this
193-
// will panic if the value is found and can't be converted to type T). So if the positional
194-
// argument is present, the typed value will be returned and the boolean will be true.
195-
// Otherwise, the zero value of type T will be returned and the boolean will be false.
196-
func LookupArg[T any](c *Command, id string) (T, bool) {
197-
for i := len(c.Args) - 1; i >= 0; i-- {
198-
if c.Args[i].ID == id {
199-
return c.Args[i].Value.(T), true
200-
}
201-
}
202-
var zero T
203-
return zero, false
204-
}
205-
206-
// GetArg gets the positional argument value with the given id in the given Command and
207-
// converts the value to the given type T through an untested type assertion. This
208-
// function will panic if the value isn't found or if the value can't be converted to type
209-
// T. To check whether the value is found instead of panicking, see [LookupArg].
210-
func GetArg[T any](c *Command, id string) T {
211-
if v, ok := LookupArg[T](c, id); ok {
212-
return v
213-
}
214-
panic("no parsed value found for positional argument id '" + id + "'")
215-
}
216-
217-
// GetArgOr looks for a positional argument value with the given id in the given Command
218-
// and converts the value to the given type T through an untested type assertion (so this
219-
// will panic if the value is found and can't be converted to type T). If the value isn't
220-
// found, the given fallback value will be returned. To check whether the value is found
221-
// instead of using a fallback value, see [LookupArg].
222-
func GetArgOr[T any](c *Command, id string, fallback T) T {
223-
if v, ok := LookupArg[T](c, id); ok {
178+
// GetOr looks for a parsed input value with the given id in the given Command and
179+
// converts the value to the given type T through an untested type assertion (so this
180+
// will panic if the value is found and can't be converted to type T). If the value
181+
// isn't found, the given fallback value will be returned. To check whether the value
182+
// is found instead of using a fallback value, see [Lookup].
183+
func GetOr[T any](c *Command, id string, fallback T) T {
184+
if v, ok := Lookup[T](c, id); ok {
224185
return v
225186
}
226187
return fallback
@@ -255,7 +216,7 @@ func (in *CommandInfo) Parse(args ...string) (*Command, error) {
255216
args = os.Args[1:]
256217
}
257218
c := &Command{
258-
Opts: make([]Input, 0, len(args)),
219+
Inputs: make([]Input, 0, len(args)),
259220
}
260221
err := parse(in, c, args)
261222
return c, err
@@ -279,17 +240,17 @@ func lookupOptionByShortName(in *CommandInfo, shortName string) *InputInfo {
279240
}
280241

281242
func hasOpt(c *Command, id string) bool {
282-
for i := range c.Opts {
283-
if c.Opts[i].ID == id {
243+
for i := range c.Inputs {
244+
if c.Inputs[i].ID == id {
284245
return true
285246
}
286247
}
287248
return false
288249
}
289250

290251
func hasArg(c *Command, id string) bool {
291-
for i := range c.Args {
292-
if c.Args[i].ID == id {
252+
for i := range c.Inputs {
253+
if c.Inputs[i].ID == id {
293254
return true
294255
}
295256
}
@@ -305,7 +266,7 @@ func parse(c *CommandInfo, p *Command, args []string) error {
305266
if err != nil {
306267
return fmt.Errorf("parsing default value '%s' for option '%s': %w", dv, c.Opts[i].ID, err)
307268
}
308-
p.Opts = append(p.Opts, pi)
269+
p.Inputs = append(p.Inputs, pi)
309270
}
310271
}
311272
for i := range c.Args {
@@ -315,7 +276,7 @@ func parse(c *CommandInfo, p *Command, args []string) error {
315276
if err != nil {
316277
return fmt.Errorf("parsing default value '%s' for arg '%s': %w", dv, c.Args[i].ID, err)
317278
}
318-
p.Args = append(p.Args, pi)
279+
p.Inputs = append(p.Inputs, pi)
319280
}
320281
}
321282

@@ -327,7 +288,7 @@ func parse(c *CommandInfo, p *Command, args []string) error {
327288
if err != nil {
328289
return fmt.Errorf("using env var '%s': %w", c.Opts[i].EnvVar, err)
329290
}
330-
p.Opts = append(p.Opts, pi)
291+
p.Inputs = append(p.Inputs, pi)
331292
}
332293
}
333294
}
@@ -338,7 +299,7 @@ func parse(c *CommandInfo, p *Command, args []string) error {
338299
if err != nil {
339300
return fmt.Errorf("using env var '%s': %w", c.Args[i].EnvVar, err)
340301
}
341-
p.Args = append(p.Args, pi)
302+
p.Inputs = append(p.Inputs, pi)
342303
}
343304
}
344305
}
@@ -406,7 +367,7 @@ func parse(c *CommandInfo, p *Command, args []string) error {
406367
}
407368
}
408369

409-
p.Opts = append(p.Opts, pi)
370+
p.Inputs = append(p.Inputs, pi)
410371

411372
if skipRest {
412373
break
@@ -470,7 +431,7 @@ func parse(c *CommandInfo, p *Command, args []string) error {
470431
}
471432
}
472433

473-
p.Opts = append(p.Opts, pi)
434+
p.Inputs = append(p.Inputs, pi)
474435
}
475436

476437
var errMissingOpts error
@@ -510,7 +471,7 @@ func parse(c *CommandInfo, p *Command, args []string) error {
510471
if err != nil {
511472
return fmt.Errorf("parsing positional argument #%d '%s': %w", i+1, rawArg, err)
512473
}
513-
p.Args = append(p.Args, pi)
474+
p.Inputs = append(p.Inputs, pi)
514475
} else if c.Args[i].IsRequired {
515476
var missing []string
516477
for ; i < len(c.Args); i++ {
@@ -539,8 +500,8 @@ func parse(c *CommandInfo, p *Command, args []string) error {
539500
return ErrNoSubcmd
540501
}
541502
p.Subcmd = &Command{
542-
Opts: make([]Input, 0, len(rest)),
543-
Name: rest[0],
503+
Inputs: make([]Input, 0, len(rest)),
504+
Name: rest[0],
544505
}
545506

546507
var subcmdInfo *CommandInfo

0 commit comments

Comments
 (0)