/
commands.go
295 lines (275 loc) · 7.8 KB
/
commands.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
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
package cmds
import (
"fmt"
"path/filepath"
"runtime"
"strings"
"sync"
log2 "github.com/cybriq/proc/pkg/log"
"github.com/cybriq/proc/pkg/opts/config"
"github.com/cybriq/proc/pkg/opts/meta"
"github.com/cybriq/proc/pkg/opts/text"
"github.com/cybriq/proc/pkg/opts/toggle"
"github.com/cybriq/proc/pkg/path"
"github.com/cybriq/proc/pkg/util"
)
type Op func(c *Command, args []string) error
var NoOp = func(c *Command, args []string) error { return nil }
var Tags = func(s ...string) []string {
return s
}
// Command is a specification for a command and can include any number of
// subcommands, and for each Command a list of options
type Command struct {
path.Path
Name string
Description string
Documentation string
Entrypoint Op
Parent *Command
Commands Commands
Configs config.Opts
Default []string // specifies default subcommand to execute
sync.Mutex
}
// Commands are a slice of Command entries
type Commands []*Command
func (c *Command) AddCommand(cm *Command) {
c.Commands = append(c.Commands, cm)
}
const configFilename = "config.toml"
// GetConfigBase creates an option set that should go in the root of a
// Command specification for an application, providing a data directory path
// and config file path.
//
// This exists in order to simplify setup for application configuration
// persistence.
func GetConfigBase(in config.Opts, appName string, abs bool) {
var defaultDataDir, defaultConfigFile string
switch runtime.GOOS {
case "linux", "aix", "freebsd", "netbsd", "openbsd", "dragonfly":
defaultDataDir = fmt.Sprintf("~/.%s", appName)
defaultConfigFile =
fmt.Sprintf("%s/%s", defaultDataDir, configFilename)
case "windows":
defaultDataDir = fmt.Sprintf("%%LOCALAPPDATA%%\\%s", appName)
defaultConfigFile =
fmt.Sprintf("%%LOCALAPPDATA%%\\%s\\%s", defaultDataDir,
configFilename)
case "darwin":
defaultDataDir = filepath.Join(
"~", "Library",
"Application Support", strings.ToUpper(appName),
)
defaultConfigFile = filepath.Join(defaultDataDir, configFilename)
}
options := config.Opts{
"ConfigFile": text.New(meta.Data{
Aliases: []string{"CF"},
Label: "Configuration File",
Description: "location of configuration file",
Documentation: strings.TrimSpace(`
The configuration file path defines the place where the configuration will be
loaded from at application startup, and where it will be written if changed.
`),
Default: defaultConfigFile,
}, text.NormalizeFilesystemPath(abs, appName)),
"DataDir": text.New(meta.Data{
Aliases: []string{"DD"},
Label: "Data Directory",
Description: "root folder where application data is stored",
Default: defaultDataDir,
}, text.NormalizeFilesystemPath(abs, appName)),
"LogCodeLocations": toggle.New(meta.Data{
Aliases: []string{"LCL"},
Label: "Log Code Locations",
Description: "whether to print code locations in logs",
Documentation: strings.TrimSpace(strings.TrimSpace(`
Toggles on and off the printing of code locations in logs.
`)),
Default: "true",
}, func(o *toggle.Opt) (err error) {
log2.CodeLoc = o.Value().Bool()
return
}),
"LogLevel": text.New(meta.Data{
Aliases: []string{"LL"},
Label: "Log Level",
Description: "Level of logging to print: [ " + log2.LvlStr.String() +
" ]",
Documentation: strings.TrimSpace(`
Log levels are values in ascending order with the following names:
` + log2.LvlStr.String() + `
The level set in this configuration item defines the limit in ascending order
of what log level printers will output. Default is 'info' which means debug and
trace log statements will not print.
`),
Default: log2.GetLevelName(log2.Info),
}, func(o *text.Opt) (err error) {
v := strings.TrimSpace(o.Value().Text())
found := false
lvl := log2.Info
for i := range log2.LvlStr {
ll := log2.GetLevelName(i)
if util.Norm(v) == strings.TrimSpace(ll) {
lvl = i
found = true
}
}
if !found {
err = fmt.Errorf("log level value %s not valid from %v",
v, log2.LvlStr)
_ = o.FromString(log2.GetLevelName(lvl))
}
log2.SetLogLevel(lvl)
return
}),
"LogFilePath": text.New(meta.Data{
Aliases: Tags("LFP"),
Label: "Log To File",
Description: "Write logs to the specified file",
Documentation: strings.TrimSpace(`
Sets the path of the file to write logs to.
`),
Default: filepath.Join(defaultDataDir, "log.txt"),
}, func(o *text.Opt) (err error) {
err = log2.SetLogFilePath(o.Expanded())
return
}, text.NormalizeFilesystemPath(abs, appName)),
"LogToFile": toggle.New(meta.Data{
Aliases: Tags("LTF"),
Label: "Log To File",
Description: "Enable writing of logs",
Documentation: strings.TrimSpace(`
Enables the writing of logs to the file path defined in LogFilePath.
`),
Default: "false",
}, func(o *toggle.Opt) (err error) {
if o.Value().Bool() {
log.T.Ln("starting log file writing")
err = log2.StartLogToFile()
} else {
err = log2.StopLogToFile()
log.T.Ln("stopped log file writing")
}
log.E.Chk(err)
return
}),
}
for i := range options {
in[i] = options[i]
}
}
// Init sets up a Command to be ready to use. Puts the reverse paths into the
// tree structure, puts sane defaults into command launchers, runs the hooks on
// all the defined configuration values, and sets the paths on each Command and
// Option so that they can be directly interrogated for their location.
func Init(c *Command, p path.Path) (cmd *Command, err error) {
if c.Parent != nil {
log.T.Ln("backlinking children of", c.Parent.Name)
}
if c.Entrypoint == nil {
c.Entrypoint = NoOp
}
if p == nil {
p = path.Path{c.Name}
}
c.Path = p // .Parent()
for i := range c.Configs {
c.Configs[i].SetPath(p)
}
for i := range c.Commands {
c.Commands[i].Parent = c
c.Commands[i].Path = p.Child(c.Commands[i].Name)
_, _ = Init(c.Commands[i], p.Child(c.Commands[i].Name))
}
c.ForEach(func(cmd *Command, _ int) bool {
for i := range cmd.Configs {
err = cmd.Configs[i].RunHooks()
if log.E.Chk(err) {
return false
}
}
return true
}, 0, 0, c)
return c, err
}
// GetOpt returns the option at a requested path
func (c *Command) GetOpt(path path.Path) (o config.Option) {
p := make([]string, len(path))
for i := range path {
p[i] = path[i]
}
switch {
case len(p) < 1:
// not found
return
case len(p) > 2:
// search subcommands
for i := range c.Commands {
if util.Norm(c.Commands[i].Name) == util.Norm(p[1]) {
return c.Commands[i].GetOpt(p[1:])
}
}
case len(p) == 2:
// check name matches path, search for config item
if util.Norm(c.Name) == util.Norm(p[0]) {
for i := range c.Configs {
if util.Norm(i) == util.Norm(p[1]) {
return c.Configs[i]
}
}
}
}
return nil
}
func (c *Command) GetCommand(p string) (o *Command) {
pp := strings.Split(p, " ")
path := path.Path(pp)
// log.I.F("%v == %v", path, c.Path)
if path.Equal(c.Path) {
// log.I.Ln("found", c.Path)
return c
}
for i := range c.Commands {
// log.I.Ln(c.Commands[i].Path)
o = c.Commands[i].GetCommand(p)
if o != nil {
return
}
}
return
}
// ForEach runs a closure on every node in the Commands tree, stopping if the
// closure returns false
func (c *Command) ForEach(cl func(*Command, int) bool, hereDepth,
hereDist int, cmd *Command) (ocl func(*Command, int) bool, depth,
dist int, cm *Command) {
ocl = cl
cm = cmd
if hereDepth == 0 {
if !ocl(cm, hereDepth) {
return
}
}
depth = hereDepth + 1
log.T.Ln(path.GetIndent(depth)+"->", depth)
dist = hereDist
for i := range c.Commands {
log.T.Ln(path.GetIndent(depth)+"walking", c.Commands[i].Name, depth,
dist)
if !cl(c.Commands[i], depth) {
return
}
dist++
ocl, depth, dist, cm = c.Commands[i].ForEach(
cl,
depth,
dist,
cm,
)
}
log.T.Ln(path.GetIndent(hereDepth)+"<-", hereDepth)
depth--
return
}