Skip to content

Commit 889645a

Browse files
committed
feat: allow for subcommand to be optional
1 parent b182164 commit 889645a

3 files changed

Lines changed: 49 additions & 6 deletions

File tree

builder.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ func (c CommandInfo) Usage(lines ...string) CommandInfo {
148148
return c
149149
}
150150

151+
// SubcmdOptional marks this CommandInfo as not requiring one of its subcommands to be
152+
// provided. By default, when parsing against a CommandInfo that has subcommands, an error
153+
// will be returned if there is no subcommand provided. However, calling this method will
154+
// change it so there will not be a parsing error if a subcommand argument is absent, and
155+
// the parsed subcommand field Subcmd on [Command] will be nil.
156+
func (c CommandInfo) SubcmdOptional() CommandInfo {
157+
c.IsSubcmdOptional = true
158+
return c
159+
}
160+
151161
// Opt adds o as an option to this CommandInfo. This method will panic if the option has
152162
// neither a long or short name set (this should never happen when using the builder
153163
// pattern starting with the [NewOpt] function or its siblings).

cli.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ type CommandInfo struct {
103103
Args []InputInfo
104104
Subcmds []CommandInfo
105105

106+
IsSubcmdOptional bool
107+
106108
isPrepped bool
107109
}
108110

@@ -578,22 +580,25 @@ func parse(c *CommandInfo, p *Command, args []string) error {
578580
}
579581

580582
if len(rest) < 1 {
583+
if c.IsSubcmdOptional {
584+
return nil
585+
}
581586
return ErrNoSubcmd
582587
}
583-
p.Subcmd = &Command{
584-
Inputs: make([]Input, 0, len(rest)),
585-
Name: rest[0],
586-
}
587588

588589
var subcmdInfo *CommandInfo
589590
for i := range c.Subcmds {
590-
if c.Subcmds[i].Name == p.Subcmd.Name {
591+
if c.Subcmds[i].Name == rest[0] {
591592
subcmdInfo = &c.Subcmds[i]
592593
break
593594
}
594595
}
595596
if subcmdInfo == nil {
596-
return UnknownSubcmdError{Name: p.Subcmd.Name}
597+
return UnknownSubcmdError{Name: rest[0]}
598+
}
599+
p.Subcmd = &Command{
600+
Inputs: make([]Input, 0, len(rest)),
601+
Name: rest[0],
597602
}
598603

599604
// If we have an error from parsing this command (from above), only return it so long

examples_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,34 @@ import (
1212
"github.com/steverusso/cli"
1313
)
1414

15+
func ExampleCommandInfo_SubcmdOptional() {
16+
// Simple command-with-subcommand structure. Parsing the top-level
17+
// command will return an error if a subcommand isn't provided.
18+
scReq := cli.New("example").
19+
Help("an example program").
20+
Subcmd(cli.NewCmd("cmd1"))
21+
22+
// Same command structure as above, except parsing will
23+
// NOT return an error if a subcommand isn't provided.
24+
scOpt := cli.New("example").
25+
Help("an example program").
26+
Subcmd(cli.NewCmd("cmd1")).
27+
SubcmdOptional()
28+
29+
c, _ := scReq.ParseThese("cmd1")
30+
fmt.Printf("have subcommand %q\n", c.Subcmd.Name)
31+
32+
_, err := scReq.ParseThese()
33+
fmt.Println("err:", err)
34+
35+
c, err = scOpt.ParseThese()
36+
fmt.Printf("err: %v, c.Subcmd: %v\n", err, c.Subcmd) // using c.Subcmd.Name here would panic
37+
// Output:
38+
// have subcommand "cmd1"
39+
// err: missing subcommand
40+
// err: <nil>, c.Subcmd: <nil>
41+
}
42+
1543
func ExampleInputInfo_Default() {
1644
in := cli.New().Opt(cli.NewIntOpt("flag").Default("1234"))
1745

0 commit comments

Comments
 (0)