-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathclic.go
229 lines (188 loc) · 6.14 KB
/
clic.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
// Package clic provides a structured multiplexer for CLI commands. In other
// words, clic will parse CLI command arguments and route callers to the
// appropriate handler.
//
// There are three kinds of command line arguments that clic helps to manage:
// Commands/Subcommands, Flags (plus related flag values), and Operands.
// Commands/subcommands each optionally have their own flags and operands. If
// an argument of a command does not match a subcommand, and is not a flag arg
// (i.e. it does not start with a hyphen and is not a flag value), then it will
// be parsed as an operand if any operands have been defined.
//
// Argument kinds and their placements:
//
// command --flag=flag-value subcommand -f flag-value operand_a operand_b
//
// Custom templates and template behaviors (i.e. template function maps) can be
// set. Custom data can be attached to instances of Clic, FlagSet, Flag,
// OperandSet, and Operand using their Meta fields for access from custom
// templates.
package clic
import (
"context"
"errors"
"slices"
"github.com/daved/clic/cerrs"
"github.com/daved/clic/flagset"
"github.com/daved/clic/operandset"
)
// Handler describes types that can be used to handle CLI command requests.
type Handler interface {
HandleCommand(context.Context) error
}
// HandlerFunc can be used to easily convert a compatible function to a Handler
// implementation.
type HandlerFunc func(context.Context) error
// HandleCommand implements Handler.
func (f HandlerFunc) HandleCommand(ctx context.Context) error {
return f(ctx)
}
// Clic manages a CLI command handler and related information.
type Clic struct {
Links
handler Handler
FlagSet *flagset.FlagSet
OperandSet *operandset.OperandSet
called bool
tmplCfg *TmplConfig
SubRequired bool
Description string
HideUsage bool
Meta map[string]any
}
// New returns an instance of Clic.
func New(h Handler, name string, subs ...*Clic) *Clic {
c := &Clic{
handler: h,
FlagSet: flagset.New(name),
OperandSet: operandset.New(name),
tmplCfg: NewDefaultTmplConfig(),
Meta: make(map[string]any),
}
c.Links = Links{
self: c,
subs: subs,
}
for _, sub := range c.Links.subs {
sub.parent = c
}
return c
}
// NewFromFunc returns an instance of Clic. Any function that is compatible with
// [HandlerFunc] will be converted automatically and used as the [Handler].
func NewFromFunc(f HandlerFunc, name string, subs ...*Clic) *Clic {
return New(f, name, subs...)
}
// Parse receives command line interface arguments, and should be run before
// HandleResolvedCmd or Link fields are used. Parse is intended to be its own
// step when using Clic so that calling code can express behavior in between
// parsing and handling.
func (c *Clic) Parse(args []string) error {
applyRecursiveFlags(c.subs, c.FlagSet)
if err := parse(c, args, c.FlagSet.FlagSet.Name()); err != nil {
return err
}
last := lastCalled(c)
if err := last.OperandSet.Parse(last.FlagSet.FlagSet.Operands()); err != nil {
return NewError(cerrs.NewParseError(err), last)
}
return nil
}
// HandleResolvedCmd runs the Handler of the command that was selected during
// Parse processing.
func (c *Clic) HandleResolvedCmd(ctx context.Context) error {
if ctx == nil {
ctx = context.Background()
}
return c.ResolvedCmd().handler.HandleCommand(ctx)
}
// Flag adds a flag option to the FlagSet. See [flagset.FlagSet.Flag] for
// details about which value types are supported.
func (c *Clic) Flag(val any, names, usage string) *flagset.Flag {
return c.FlagSet.Flag(val, names, usage)
}
// FlagRecursive adds a flag option to the FlagSet. Recursive flags are not
// visible in the FlagSet instances of child Clic instances before Parse is
// called (i.e. recursive flags are applied to child flagsets in Parse). See
// [flagset.FlagSet.Flag] for details about which value types are supported.
func (c *Clic) FlagRecursive(val any, names, usage string) *flagset.Flag {
return c.FlagSet.FlagRecursive(val, names, usage)
}
// Operand adds an Operand option to the OperandSet. See
// [operandset.OperandSet.Operand] for details about which value types are
// supported.
func (c *Clic) Operand(val any, req bool, name, desc string) *operandset.Operand {
return c.OperandSet.Operand(val, req, name, desc)
}
// SetUsageTemplating is used to override the base template text, and provide a
// custom FuncMap. If a nil FuncMap is provided, no change will be made to the
// existing value.
func (c *Clic) SetUsageTemplating(tmplCfg *TmplConfig) {
c.tmplCfg = tmplCfg
}
// Usage returns the executed usage template. The Meta fields of the relevant
// types can be leveraged to convey detailed info/behavior in a custom template.
func (c *Clic) Usage() string {
data := &TmplData{
ResolvedCmd: c,
ResolvedCmdSet: resolvedCmdSet(c),
}
return executeTmpl(c.tmplCfg, data)
}
var errParseNoMatch = errors.New("parse: no command match")
func parse(c *Clic, args []string, cmdName string) (err error) {
fs := c.FlagSet.FlagSet
c.called = cmdName == "" || cmdName == fs.Name()
if !c.called {
return errParseNoMatch
}
if err := fs.Parse(args); err != nil {
return NewError(cerrs.NewParseError(err), c)
}
subCmdArgs := fs.Operands()
if len(subCmdArgs) == 0 {
if c.SubRequired {
return NewError(cerrs.NewParseError(cerrs.NewSubRequiredError()), c)
}
return nil
}
subCmdName := subCmdArgs[0]
subCmdArgs = subCmdArgs[1:]
for _, sub := range c.Links.subs {
if err := parse(sub, subCmdArgs, subCmdName); err != nil {
if errors.Is(err, errParseNoMatch) {
continue
}
return err
}
return nil
}
if c.SubRequired {
return NewError(cerrs.NewParseError(cerrs.NewSubRequiredError()), c)
}
return nil
}
func lastCalled(c *Clic) *Clic {
for _, sub := range c.Links.subs {
if sub.called {
return lastCalled(sub)
}
}
return c
}
func resolvedCmdSet(c *Clic) []*Clic {
all := []*Clic{c}
for c.parent != nil {
c = c.parent
all = append(all, c)
}
slices.Reverse(all)
return all
}
func applyRecursiveFlags(subs []*Clic, src *flagset.FlagSet) {
for _, sub := range subs {
flagset.ApplyRecursiveFlags(sub.FlagSet, src)
applyRecursiveFlags(sub.Links.subs, src)
applyRecursiveFlags(sub.Links.subs, sub.FlagSet)
}
}