-
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathcmdr.go
More file actions
352 lines (326 loc) · 11.3 KB
/
cmdr.go
File metadata and controls
352 lines (326 loc) · 11.3 KB
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
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
// Copyright © 2022 Hedzr Yeh.
package cmdr
import (
"context"
"os"
"strings"
"sync"
"gopkg.in/hedzr/errors.v3"
"github.com/hedzr/store"
"github.com/hedzr/store/radix"
"github.com/hedzr/cmdr/v2/builder"
"github.com/hedzr/cmdr/v2/cli"
"github.com/hedzr/cmdr/v2/cli/worker"
"github.com/hedzr/cmdr/v2/pkg/logz"
)
// func NewOpt[T any](defaultValue ...T) config.Opt {
// return nil
// }
// New starts a new cmdr app.
//
// With the returned builder.App, you may build root and sub-commands fluently.
//
// app := cmdr.New().
// Info("demo-app", "0.3.1").
// Author("hedzr")
// app.Cmd("jump").
// Description("jump command").
// Examples(`jump example`).
// Deprecated(`jump is a demo command`).
// With(func(b cli.CommandBuilder) {
// b.Hidden(false)
// b.Cmd("to").
// Description("to command").
// Examples(``).
// Deprecated(`v0.1.1`).
// Hidden(false).
// OnAction(func(cmd *cli.CmdS, args []string) (err error) {
// main1()
// return // handling command action here
// }).
// With(func(b cli.CommandBuilder) {
// b.Flg("full", "f").
// Default(false).
// Description("full command").
// Build()
// })
// })
// app.Flg("dry-run", "n").
// Default(false).
// Build() // no matter even if you're adding the duplicated one.
//
// // // simple run the parser of app and trigger the matched command's action
// // _ = app.Run(
// // cmdr.WithForceDefaultAction(false), // true for debug in developing time
// // )
//
// if err := app.Run(
// cmdr.WithForceDefaultAction(false), // true for debug in developing time
// ); err != nil {
// logz.ErrorContext(ctx, "Application Error:", "err", err)
// }
//
// After the root command and all its children are built, use app.[config.App.Run]
// to parse end-user's command-line arguments, and invoke the bound
// action on the hit subcommand.
//
// It is not necessary to attach an action onto a parent command, because
// its subcommands are the main characters - but you still can do that.
func New(opts ...cli.Opt) cli.App {
earlierInitForNew()
cfg := cli.NewConfig(opts...)
w := worker.New(cfg)
return builder.New(w)
}
// NewAppWithConfig creates the unique [App] object by passing a
// *[cli.Config] argument.
func NewAppWithConfig(cfg *cli.Config) cli.App {
earlierInitForNew()
w := worker.New(cfg)
return builder.New(w)
}
func earlierInitForNew() {
earlierInitForNewOnce.Do(func() {
_ = os.Setenv("CMDR_VERSION", Version)
logz.Verbose("setup env-var at earlier time", "CMDR_VERSION", Version)
})
}
var earlierInitForNewOnce sync.Once
// App returns a light version of [builder.Runner] (a.k.a. *[worker.Worker]).
//
// Generally it's a unique instance in a system.
//
// It's available once New() / Exec() called, else nil.
//
// App returns a [cli.Runner] instance, which is different with [builder.App].
//
// In most cases, App() return the exact app object (a &appS{} instance).
// But it's not the real worker if you're requesting shared app instance.
// A shared app instance must be made by New() and valued context:
//
// ctx := context.Background()
// ctx = context.WithValue(ctx, "shared.cmdr.app", true)
//
// app := cmdr.Create(appName, version, author, desc).
// WithAdders(cmd.Commands...).
// OnAction(func(ctx context.Context, cmd cli.Cmd, args []string) (err error) {
// fmt.Printf("app.name = %s\n", cmdr.App().Name())
// fmt.Printf("app.unique = %v\n", cmdr.App()) // return an uncertain app object
// app := cmd.Root().App() // this is the real app object associate with current RootCommand
// fmt.Printf("app = %v\n", app)
// return
// }).
// Build()
//
// if err := app.Run(ctx); err != nil {
// logz.ErrorContext(ctx, "Application Error:", "err", err) // stacktrace if in debug mode/build
// os.Exit(app.SuggestRetCode())
// } else if rc := app.SuggestRetCode(); rc != 0 {
// os.Exit(rc)
// }
//
// What effect words with a shared app object?
//
// Suppose you have a standard app running, and also you're requesting
// to build a new child app instance for some special purpose, thought
// you could declare it as a shared instance so that the main app never
// be replaced with the new one.
func App() cli.Runner { return worker.UniqueWorker() }
func AppName() string { return App().Name() } // the app's name
func AppVersion() string { return App().Version() } // the app's version
func AppDescription() string { return App().Root().Desc() } // the app's short description
func AppDescriptionLong() string { return App().Root().DescLong() } // the app's long description
// CmdLines returns the whole command-line as space-separated slice.
func CmdLines() []string { return App().Args() }
func Args() []string { return ParsedPositionalArgs() } // the remained positonal args after parsed
// Error returns all errors occurred with a leading parent.
//
// Suppose a long term action raised many errors in runtime,
// these errors will be collected and bundled as one package
// so that they could be traced at the app terminating time.
func Error() errors.Error { return App().Error() }
// Recycle collects your errors into a container.
// You can retrieve the error container with Error() later.
func Recycle(errs ...error) { App().Recycle(errs...) }
// CancelFunc returns the cancel func (ctx) managed by current worker.
func CancelFunc() func() { return App().CancelFunc() }
// Parsed identify cmdr.v2 ended the command-line arguments
// parsing task.
func Parsed() bool { return App().ParsedState() != nil } // is parsed ok?
func ParsedLastCmd() cli.Cmd { return App().ParsedState().LastCmd() } // the parsed last command
func ParsedCommands() []cli.Cmd { return App().ParsedState().MatchedCommands() } // the parsed commands
func ParsedPositionalArgs() []string { return App().ParsedState().PositionalArgs() } // the rest positional args
func ParsedState() cli.ParsedState { return App().ParsedState() } // return the parsed state
// LoadedSources records all external sources loaded ok.
//
// Each type LoadedSources is a map which collect the children
// objects if a external source loaded them successfully.
//
// LoadedSources() is an array to represent all loaders.
func LoadedSources() []cli.LoadedSources { return App().LoadedSources() } // the loaded config files or other sources
// Store returns the child Store tree at location 'app.cmd'.
//
// By default, cmdr maintains all command-line subcommands and flags
// as a child tree in the associated Set ("store") internally.
//
// You can check out the flags state by querying in this child store.
//
// For example, we have a command 'server'->'start' and its
// flag 'foreground', therefore we can query the flag what if
// it was given by user's 'app server start --foreground':
//
// fore := cmdr.Store().MustBool("server.start.foreground", false)
// if fore {
// runRealMain()
// } else {
// service.Start("start", runRealMain) // start the real main as a service
// }
//
// // second form:
// cs := cmdr.Store("server.start")
// fore := cs.MustBool("foreground")
// port := cs.MustInt("port", 7893)
//
// Q: How to inspect the internal Store()?
//
// A: Running `app [any subcommands] [any options] ~~debug` will dump
// the internal Store() tree.
//
// Q: Can I list all subcommands?
//
// A: A end-user can run `app ~~tree` in the shell to list them.
// Also, `app -v ~~tree` or `app ~~tree -vvv` can get a list
// of subcommands tree, and with those builtin hidden
// commands, and with those vendor hidden commands. In
// this case, `-vvv` dumps the hidden commands and
// vendor-hidden commands.
//
// Since v2.1.16, the passing prefix parameters will be
// joint as a dottedPath with dot char.
// So `Set("a", "b", "c")` is equivelant with `Set("a.b.c")`.
func Store(prefix ...string) store.Store {
switch len(prefix) {
case 0:
return Set(cli.CommandsStoreKey)
case 1:
return Set(cli.CommandsStoreKey, prefix[0])
default:
return Set(append([]string{cli.CommandsStoreKey}, prefix...)...)
}
}
// Set returns the `app` subtree as a KVStore associated
// with current App().
//
// conf := cmdr.Set()
// cmdStore := conf.WithPrefix(cli.CommandsStoreKey)
// assert(cmdrStore == cmdr.Store())
//
// Set() can be used for accessing the app settings.
//
// cmdr will load and merge all found external config
// files and sources into Set/Store.
//
// So, if you have a config file at `/etc/<app>/<app>.toml`
// with the following content,
//
// [logging]
// file = "/var/log/app/stdout.log"
//
// The file will be loaded to `app.logging`. Now you
// can access it with this,
//
// println(cmdr.Set().MustString("logging.file"))
//
// NOTE:
//
// To enable external source loading mechanism, you may
// need to use `hedzr/cmdr-loader`. Please surf it
// for more detail.
//
// You can also create your own external source if absent.
//
// Since v2.1.16, the passing prefix parameters will be
// joint as a dottedPath with dot char.
// So `Set("a", "b", "c")` is equivelant with `Set("a.b.c")`.
func Set(prefix ...string) store.Store {
if len(prefix) == 0 {
return App().Store()
}
var pre = strings.Join(prefix, ".")
if pre == "" {
return App().Store() // .WithPrefix(cli.DefaultStoreKeyPrefix)
}
return App().Store().WithPrefix(pre)
}
const DefaultStorePrefix = cli.DefaultStoreKeyPrefix
const CommandsStoreKey = cli.CommandsStoreKey
// RemoveOrderedPrefix removes '[a-z0-9]+\.' at front of string.
func RemoveOrderedPrefix(s string) string {
return cli.RemoveOrderedPrefix(s)
}
// DottedPathToCommandOrFlag searches the matched CmdS or
// Flag with the specified dotted-path.
//
// anyCmd is the starting of this searching.
// Give it a nil if you have no idea.
func DottedPathToCommandOrFlag(dottedPath string, anyCmd cli.Backtraceable) (cc cli.Backtraceable, ff *cli.Flag) {
if anyCmd == nil {
anyCmd = App().Root()
}
return cli.DottedPathToCommandOrFlag1(dottedPath, anyCmd)
}
// To finds a given path and loads the subtree into
// 'holder', typically 'holder' could be a struct.
//
// For yaml input
//
// app:
// server:
// sites:
// - name: default
// addr: ":7999"
// location: ~/Downloads/w/docs
//
// The following codes can load it into sitesS struct:
//
// var sites sitesS
// err = cmdr.To("server.sites", &sites)
//
// type sitesS struct{ Sites []siteS }
//
// type siteS struct {
// Name string
// Addr string
// Location string
// }
//
// In this above case, 'store' loaded yaml and built it
// into memory, and extract 'server.sites' into 'sitesS'.
// Since 'server.sites' is a yaml array, it was loaded
// as a store entry and holds a slice value, so GetSectionFrom
// extract it to sitesS.Sites field.
//
// The optional MOpt operators could be:
// - WithKeepPrefix
// - WithFilter
func To[T any](path string, holder *T, opts ...radix.MOpt[any]) (err error) {
return App().Store().To(path, holder, opts...)
}
// Exec starts a new cmdr app (parsing cmdline args based on the given rootCmd)
// from scratch.
//
// It's a reserved API for back-compatible with cmdr v1.
//
// It'll be removed completely at the recently future version.
//
// Deprecated since 2.1 by app.Run()
func Exec(rootCmd *cli.RootCommand, opts ...cli.Opt) (err error) {
// if is.InDebugging() {
// _ = exec.Run("/bin/false")
// // cabin.Version()
// // cpcn.Out()
// }
app := New(opts...).SetRootCommand(rootCmd)
err = app.Run(context.Background())
return
}