Skip to content

Commit 40965a9

Browse files
committed
argparse: add parsing for subcommands
Add handling for subcommands in the custom arg parser. To specify a subcommand, a user can call 'parser.Subcommand' and provide an 'argparse.Subcommand' instance, whose 'Name()' is used to identify its use on the command line. When the specified subcommand is identifies, its 'Run()' function can be called in 'InvokeSubcommand()'. This function, when called, is provided the remaining arguments *after* the subcommand, to allow the subcommand function to perform its own argument parsing. To use the most accurate terminology in usage printouts, include a function ('SetIsTopLevel()') to toggle whether the subcommands listed in the printout are called "Commands" or "Subcommands". Signed-off-by: Victoria Dye <[email protected]>
1 parent 663895c commit 40965a9

File tree

1 file changed

+98
-1
lines changed

1 file changed

+98
-1
lines changed

internal/argparse/argparse.go

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,129 @@ import (
44
"flag"
55
"fmt"
66
"os"
7+
"strings"
78
)
89

910
type argParser struct {
11+
// State
12+
isTopLevel bool
13+
parsed bool
14+
argOffset int
15+
16+
// Pre-parsing
17+
subcommands map[string]Subcommand
18+
19+
// Post-parsing
20+
selectedSubcommand Subcommand
21+
1022
flag.FlagSet
1123
}
1224

1325
func NewArgParser(usageString string) *argParser {
1426
flagSet := flag.NewFlagSet("", flag.ExitOnError)
1527

1628
a := &argParser{
17-
FlagSet: *flagSet,
29+
isTopLevel: false,
30+
parsed: false,
31+
argOffset: 0,
32+
subcommands: make(map[string]Subcommand),
33+
FlagSet: *flagSet,
1834
}
1935

2036
a.FlagSet.Usage = func() {
2137
out := a.FlagSet.Output()
2238
fmt.Fprintf(out, "usage: %s\n\n", usageString)
39+
40+
// Print subcommands (if any)
41+
if len(a.subcommands) > 0 {
42+
if a.isTopLevel {
43+
fmt.Fprintln(out, "Commands:")
44+
} else {
45+
fmt.Fprintln(out, "Subcommands:")
46+
}
47+
a.printSubcommands()
48+
fmt.Fprint(out, "\n")
49+
}
2350
}
2451

2552
return a
2653
}
2754

55+
func (a *argParser) SetIsTopLevel(isTopLevel bool) {
56+
a.isTopLevel = isTopLevel
57+
}
58+
59+
func (a *argParser) printSubcommands() {
60+
out := a.FlagSet.Output()
61+
for _, subcommand := range a.subcommands {
62+
fmt.Fprintf(out, " %s\n \t%s\n",
63+
subcommand.Name(),
64+
strings.ReplaceAll(strings.TrimSpace(subcommand.Description()), "\n", "\n \t"),
65+
)
66+
}
67+
}
68+
69+
func (a *argParser) Subcommand(subcommand Subcommand) {
70+
a.subcommands[subcommand.Name()] = subcommand
71+
}
72+
2873
func (a *argParser) Parse(args []string) {
74+
if a.parsed {
75+
// Do nothing if we've already parsed args
76+
return
77+
}
78+
2979
err := a.FlagSet.Parse(args)
3080
if err != nil {
3181
panic("argParser FlagSet error handling should be 'ExitOnError', but error encountered")
3282
}
83+
84+
if len(a.subcommands) > 0 {
85+
// Parse subcommand, if applicable
86+
if a.FlagSet.NArg() == 0 {
87+
a.Usage("Please specify a subcommand")
88+
}
89+
90+
subcommand, exists := a.subcommands[a.FlagSet.Arg(0)]
91+
if !exists {
92+
a.Usage("Invalid subcommand '%s'", a.FlagSet.Arg(0))
93+
} else {
94+
a.selectedSubcommand = subcommand
95+
a.argOffset++
96+
}
97+
} else {
98+
if a.NArg() != 0 {
99+
// If not using subcommands, all args should be accounted for
100+
// Exit with usage if not
101+
a.Usage("Unused arguments specified: %s", strings.Join(a.Args(), " "))
102+
}
103+
}
104+
105+
a.parsed = true
106+
}
107+
108+
func (a *argParser) Arg(index int) string {
109+
return a.FlagSet.Arg(index + a.argOffset)
110+
}
111+
112+
func (a *argParser) Args() []string {
113+
return a.FlagSet.Args()[a.argOffset:]
114+
}
115+
116+
func (a *argParser) NArg() int {
117+
if a.FlagSet.NArg() <= a.argOffset {
118+
return 0
119+
} else {
120+
return a.FlagSet.NArg() - a.argOffset
121+
}
122+
}
123+
124+
func (a *argParser) InvokeSubcommand() error {
125+
if !a.parsed || a.selectedSubcommand == nil {
126+
panic("subcommand has not been parsed")
127+
}
128+
129+
return a.selectedSubcommand.Run(a.Args())
33130
}
34131

35132
func (a *argParser) Usage(errFmt string, args ...any) {

0 commit comments

Comments
 (0)