1 // Package podopts is a configuration system to fit with the all-in-one philosophy guiding the design of the parallelcoin
2 // pod.
3 //
4 // The configuration is stored by each component of the connected applications, so all data is stored in concurrent-safe
5 // atomics, and there is a facility to invoke a function in response to a new value written into a field by other
6 // threads.
7 //
8 // There is a custom JSON marshal/unmarshal for each field type and for the whole configuration that only saves values
9 // that differ from the defaults, similar to 'omitempty' in struct tags but where 'empty' is the default value instead
10 // of the default zero created by Go's memory allocator. This enables easy compositing of multiple sources.
11 //
12 package config
13 14 import (
15 "encoding/hex"
16 "encoding/json"
17 "fmt"
18 "io/ioutil"
19 "os"
20 "os/user"
21 "path/filepath"
22 "reflect"
23 "sort"
24 "strings"
25 "time"
26 "unicode/utf8"
27 28 "lukechampine.com/blake3"
29 30 "github.com/p9c/p9/pkg/log"
31 "github.com/p9c/p9/pkg/apputil"
32 "github.com/p9c/p9/pkg/constant"
33 "github.com/p9c/p9/pkg/opts/binary"
34 "github.com/p9c/p9/pkg/opts/cmds"
35 "github.com/p9c/p9/pkg/opts/duration"
36 "github.com/p9c/p9/pkg/opts/float"
37 "github.com/p9c/p9/pkg/opts/integer"
38 "github.com/p9c/p9/pkg/opts/list"
39 "github.com/p9c/p9/pkg/opts/opt"
40 "github.com/p9c/p9/pkg/opts/text"
41 )
42 43 // Configs is the source location for the Config items, which is used to generate the Config struct
44 type Configs map[string]opt.Option
45 type ConfigSliceElement struct {
46 Opt opt.Option
47 Name string
48 }
49 type ConfigSlice []ConfigSliceElement
50 51 func (c ConfigSlice) Len() int { return len(c) }
52 func (c ConfigSlice) Less(i, j int) bool { return c[i].Name < c[j].Name }
53 func (c ConfigSlice) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
54 55 // Initialize loads in configuration from disk and from environment on top of the default base
56 func (c *Config) Initialize(hf func(ifc interface{}) error) (e error) {
57 // the several places configuration is sourced from are overlaid in the following order:
58 // default -> config file -> environment variables -> commandline flags
59 T.Ln("initializing configuration...")
60 // first lint the configuration
61 var aos map[string][]string
62 if aos, e = getAllOptionStrings(c); E.Chk(e) {
63 return
64 }
65 // this function will panic if there is potential for ambiguity in the commandline configuration args.
66 T.Ln("linting configuration items")
67 if _, e = findConflictingItems(aos); E.Chk(e) {
68 }
69 // generate and add the help commands to the help tree
70 c.GetHelp(hf)
71 // process the commandline
72 T.Ln("processing commandline arguments", os.Args[1:])
73 var cm *cmds.Command
74 var options []opt.Option
75 var optVals []string
76 if c.ExtraArgs, cm, options, optVals, e = c.processCommandlineArgs(os.Args[1:]); E.Chk(e) {
77 return
78 }
79 // I.S(options)
80 if cm != nil {
81 c.RunningCommand = *cm
82 }
83 // if the user sets the configfile directly, or the datadir on the commandline we need to load it from that path
84 T.Ln("checking from where to load the configuration file")
85 datadir := c.DataDir.V()
86 var configPath string
87 for i := range options {
88 if options[i].Name() == "configfile" {
89 if _, e = options[i].ReadInput(optVals[i]); E.Chk(e) {
90 configPath = optVals[i]
91 }
92 }
93 if options[i].Name() == "datadir" {
94 I.Ln("datadir was set", optVals[i])
95 if _, e = options[i].ReadInput(optVals[i]); !E.Chk(e) {
96 datadir = options[i].Type().(*text.Opt).V()
97 // reset all defaults that base on the datadir to apply hereafter
98 // if the value is default, update it to the new datadir, and update the default field, otherwise assume
99 // it has been set in the commandline args, and if it is different in environment or config file
100 // it will be loaded in next, with the command line option value overriding at the end.
101 if c.CAFile.V() == c.CAFile.Def {
102 e = c.CAFile.Set(filepath.Join(datadir, "ca.cert"))
103 }
104 c.CAFile.Def = filepath.Join(datadir, "ca.cert")
105 if c.ConfigFile.V() == c.ConfigFile.Def {
106 e = c.ConfigFile.Set(filepath.Join(datadir, "pod.json"))
107 }
108 c.ConfigFile.Def = filepath.Join(datadir, constant.PodConfigFilename)
109 if c.RPCKey.V() == c.RPCKey.Def {
110 e = c.RPCKey.Set(filepath.Join(datadir, "rpc.key"))
111 }
112 c.RPCKey.Def = filepath.Join(datadir, "rpc.key")
113 if c.RPCCert.V() == c.RPCCert.Def {
114 e = c.RPCCert.Set(filepath.Join(datadir, "rpc.cert"))
115 }
116 c.RPCCert.Def = filepath.Join(datadir, "rpc.cert")
117 }
118 }
119 }
120 for i := range options {
121 if options[i].Name() == "network" {
122 I.Ln("network was set", optVals[i])
123 if c.WalletFile.V() == c.WalletFile.Def {
124 _, e = c.WalletFile.ReadInput(filepath.Join(datadir, optVals[i], "wallet.db"))
125 }
126 c.WalletFile.Def = filepath.Join(datadir, optVals[i], constant.DbName)
127 if c.LogDir.V() == c.LogDir.Def {
128 _, e = c.LogDir.ReadInput(filepath.Join(datadir, optVals[i]))
129 }
130 c.LogDir.Def = filepath.Join(datadir, optVals[i])
131 }
132 }
133 // load the configuration file into the config
134 resolvedConfigPath := c.ConfigFile.V()
135 if configPath != "" {
136 T.Ln("loading config from", configPath)
137 resolvedConfigPath = configPath
138 } else {
139 if datadir != "" {
140 if strings.HasPrefix(datadir, "~") {
141 var homeDir string
142 var usr *user.User
143 var e error
144 if usr, e = user.Current(); e == nil {
145 homeDir = usr.HomeDir
146 }
147 // Fall back to standard HOME environment variable that works for most POSIX OSes if the directory from the Go
148 // standard lib failed.
149 if e != nil || homeDir == "" {
150 homeDir = os.Getenv("HOME")
151 }
152 153 datadir = strings.Replace(datadir, "~", homeDir, 1)
154 }
155 156 if resolvedConfigPath, e = filepath.Abs(filepath.Clean(filepath.Join(datadir, constant.PodConfigFilename,
157 ),
158 ),
159 ); E.Chk(e) {
160 }
161 T.Ln("loading config from", resolvedConfigPath)
162 }
163 }
164 var configExists bool
165 if e = c.loadConfig(resolvedConfigPath); !D.Chk(e) {
166 configExists = true
167 }
168 // read the environment variables into the config
169 I.Ln("reading environment variables...")
170 if e = c.loadEnvironment(); D.Chk(e) {
171 }
172 // read in the commandline options over top as they have highest priority
173 I.Ln("decoding option inputs")
174 for i := range options {
175 if _, e = options[i].ReadInput(optVals[i]); E.Chk(e) {
176 }
177 }
178 if !configExists || c.Save.True() {
179 c.Save.F()
180 // save the configuration file
181 I.Ln("saving configuration file...")
182 var j []byte
183 // c.ShowAll=true
184 if j, e = json.MarshalIndent(c, "", " "); !E.Chk(e) {
185 I.F("saving config\n%s\n", string(j))
186 apputil.EnsureDir(resolvedConfigPath)
187 if e = ioutil.WriteFile(resolvedConfigPath, j, 0660); E.Chk(e) {
188 panic(e)
189 }
190 }
191 I.Ln("configuration file saved")
192 }
193 I.Ln("configuration initialised")
194 return
195 }
196 197 // loadEnvironment scans the environment variables for values relevant to pod
198 func (c *Config) loadEnvironment() (e error) {
199 env := os.Environ()
200 c.ForEach(func(o opt.Option) bool {
201 varName := "POD_" + strings.ToUpper(o.Name())
202 for i := range env {
203 if strings.HasPrefix(env[i], varName) {
204 envVal := strings.Split(env[i], varName)[1]
205 if _, e = o.LoadInput(envVal); D.Chk(e) {
206 }
207 }
208 }
209 return true
210 },
211 )
212 213 return
214 }
215 216 // loadConfig loads the config from a file and unmarshals it into the config
217 func (c *Config) loadConfig(path string) (e error) {
218 e = fmt.Errorf("no config found at %s", path)
219 var cf []byte
220 if !apputil.FileExists(path) {
221 return
222 } else if cf, e = ioutil.ReadFile(path); !D.Chk(e) {
223 I.Ln("read in file from", path)
224 if e = json.Unmarshal(cf, c); D.Chk(e) {
225 panic(e)
226 }
227 I.Ln("unmarshalled", path)
228 c.WalletPass.Set("")
229 }
230 return
231 }
232 233 // WriteToFile writes the current config to a file as json
234 func (c *Config) WriteToFile(filename string) (e error) {
235 var j []byte
236 I.S(c.MulticastPass.Bytes())
237 wpp := c.WalletPass.Bytes()
238 wp := make([]byte, len(wpp))
239 copy(wp,wpp)
240 if len(wp) > 0 {
241 bhb := blake3.Sum256(wpp)
242 bh := hex.EncodeToString(bhb[:])
243 c.WalletPass.Set(bh)
244 }
245 if j, e = json.MarshalIndent(c, "", " "); E.Chk(e) {
246 return
247 }
248 if e = ioutil.WriteFile(filename, j, 0660); E.Chk(e) {
249 }
250 if len(wp) > 0 {
251 c.WalletPass.SetBytes(wp)
252 }
253 return
254 }
255 256 // ForEach iterates the options in defined order with a closure that takes an opt.Option
257 func (c *Config) ForEach(fn func(ifc opt.Option) bool) bool {
258 t := reflect.ValueOf(c)
259 t = t.Elem()
260 for i := 0; i < t.NumField(); i++ {
261 // asserting to an Option ensures we skip the ancillary fields
262 if iff, ok := t.Field(i).Interface().(opt.Option); ok {
263 if !fn(iff) {
264 return false
265 }
266 }
267 }
268 return true
269 }
270 271 // GetOption searches for a match amongst the podopts
272 func (c *Config) GetOption(input string) (
273 op opt.Option, value string,
274 e error,
275 ) {
276 T.Ln("checking arg for opt:", input)
277 found := false
278 if c.ForEach(func(ifc opt.Option) bool {
279 aos := ifc.GetAllOptionStrings()
280 for i := range aos {
281 if strings.HasPrefix(input, aos[i]) {
282 value = input[len(aos[i]):]
283 found = true
284 op = ifc
285 return false
286 }
287 }
288 return true
289 },
290 ) {
291 }
292 if !found {
293 e = fmt.Errorf("opt not found")
294 }
295 return
296 }
297 298 // MarshalJSON implements the json marshaller for the config. It only stores non-default values so can be composited.
299 func (c *Config) MarshalJSON() (b []byte, e error) {
300 outMap := make(map[string]interface{})
301 c.ForEach(
302 func(ifc opt.Option) bool {
303 switch ii := ifc.(type) {
304 case *binary.Opt:
305 if ii.True() == ii.Def && ii.Data.OmitEmpty && !c.ShowAll {
306 return true
307 }
308 outMap[ii.Option] = ii.True()
309 case *list.Opt:
310 v := ii.S()
311 if len(v) == len(ii.Def) && ii.Data.OmitEmpty && !c.ShowAll {
312 foundMismatch := false
313 for i := range v {
314 if v[i] != ii.Def[i] {
315 foundMismatch = true
316 break
317 }
318 }
319 if !foundMismatch {
320 return true
321 }
322 }
323 outMap[ii.Option] = v
324 case *float.Opt:
325 if ii.Value.Load() == ii.Def && ii.Data.OmitEmpty && !c.ShowAll {
326 return true
327 }
328 outMap[ii.Option] = ii.Value.Load()
329 case *integer.Opt:
330 if ii.Value.Load() == ii.Def && ii.Data.OmitEmpty && !c.ShowAll {
331 return true
332 }
333 outMap[ii.Option] = ii.Value.Load()
334 case *text.Opt:
335 v := string(ii.Value.Load().([]byte))
336 // fmt.Printf("def: '%s'", v)
337 // spew.Dump(ii.def)
338 if v == ii.Def && ii.Data.OmitEmpty && !c.ShowAll {
339 return true
340 }
341 outMap[ii.Option] = v
342 case *duration.Opt:
343 if ii.Value.Load() == ii.Def && ii.Data.OmitEmpty && !c.ShowAll {
344 return true
345 }
346 outMap[ii.Option] = fmt.Sprint(ii.Value.Load())
347 default:
348 }
349 return true
350 },
351 )
352 return json.Marshal(&outMap)
353 }
354 355 // UnmarshalJSON implements the Unmarshaller interface so it only writes to fields with those non-default values set.
356 func (c *Config) UnmarshalJSON(data []byte) (e error) {
357 ifc := make(map[string]interface{})
358 if e = json.Unmarshal(data, &ifc); E.Chk(e) {
359 return
360 }
361 // I.S(ifc)
362 c.ForEach(func(iii opt.Option) bool {
363 switch ii := iii.(type) {
364 case *binary.Opt:
365 if i, ok := ifc[ii.Option]; ok {
366 var ir bool
367 if ir, ok = i.(bool); ir != ii.Def {
368 // I.Ln(ii.Option+":", i.(binary), "default:", ii.def, "prev:", c.Map[ii.Option].(*Opt).True())
369 ii.Set(ir)
370 }
371 }
372 case *list.Opt:
373 matched := true
374 if d, ok := ifc[ii.Option]; ok {
375 if ds, ok2 := d.([]interface{}); ok2 {
376 for i := range ds {
377 if len(ii.Def) >= len(ds) {
378 if ds[i] != ii.Def[i] {
379 matched = false
380 break
381 }
382 } else {
383 matched = false
384 }
385 }
386 if matched {
387 return true
388 }
389 // I.Ln(ii.Option+":", ds, "default:", ii.def, "prev:", c.Map[ii.Option].(*Opt).S())
390 ii.Set(ifcToStrings(ds))
391 }
392 }
393 case *float.Opt:
394 if d, ok := ifc[ii.Option]; ok {
395 // I.Ln(ii.Option+":", d.(float64), "default:", ii.def, "prev:", c.Map[ii.Option].(*Opt).V())
396 ii.Set(d.(float64))
397 }
398 case *integer.Opt:
399 if d, ok := ifc[ii.Option]; ok {
400 // I.Ln(ii.Option+":", int64(d.(float64)), "default:", ii.def, "prev:", c.Map[ii.Option].(*Opt).V())
401 ii.Set(int(d.(float64)))
402 }
403 case *text.Opt:
404 if d, ok := ifc[ii.Option]; ok {
405 if ds, ok2 := d.(string); ok2 {
406 if ds != ii.Def {
407 // I.Ln(ii.Option+":", d.(string), "default:", ii.def, "prev:", c.Map[ii.Option].(*Opt).V())
408 ii.Set(d.(string))
409 }
410 }
411 }
412 case *duration.Opt:
413 if d, ok := ifc[ii.Option]; ok {
414 var parsed time.Duration
415 parsed, e = time.ParseDuration(d.(string))
416 // I.Ln(ii.Option+":", parsed, "default:", ii.Opt(), "prev:", c.Map[ii.Option].(*Opt).V())
417 ii.Set(parsed)
418 }
419 default:
420 }
421 return true
422 },
423 )
424 return
425 }
426 427 func (c *Config) processCommandlineArgs(args []string) (
428 remArgs []string, cm *cmds.Command, op []opt.Option,
429 optVals []string, e error,
430 ) {
431 // first we will locate all the commands specified to mark the 3 sections, opt, commands, and the remainder is
432 // arbitrary for the node
433 commands := make(map[int]cmds.Command)
434 var commandsStart, commandsEnd int = -1, -1
435 var found, helpFound bool
436 for i := range args {
437 T.Ln("checking for commands:", args[i], commandsStart, commandsEnd, "current arg index:", i)
438 var depth, dist int
439 if found, depth, dist, cm, e = c.Commands.Find(args[i], depth, dist, false); E.Chk(e) {
440 continue
441 }
442 if cm != nil {
443 if cm.Name == "help" {
444 helpFound = true
445 }
446 if cm.Parent != nil {
447 if cm.Parent.Name == "help" && !helpFound {
448 // I.S(commands)
449 if commands[0].Name != "help" {
450 found = false
451 }
452 }
453 }
454 }
455 if found {
456 if commandsStart == -1 {
457 commandsStart = i
458 commandsEnd = i + 1
459 }
460 if oc, ok := commands[depth]; ok {
461 if !helpFound {
462 e = fmt.Errorf("second command found at same depth '%s' and '%s'", oc.Name, cm.Name)
463 return
464 } else {
465 // if this is a command match after help is found it is a
466 // help command, resume the search
467 if cmdd, ok := commands[depth]; ok {
468 I.Ln("base level command is already occupied by",
469 cmdd.Name+"; marking end of commands")
470 // commandsEnd--
471 T.Ln("commandStart", commandsStart, commandsEnd, args[commandsStart:commandsEnd])
472 break
473 }
474 I.Ln("found help subcommand:", cm.Name, depth)
475 depth--
476 dist++
477 if found, depth, dist, cm, e = c.Commands.Find(args[i],
478 depth, dist, true); E.Chk(e) {
479 I.Ln("we didn't attach this help command")
480 continue
481 }
482 // I.S(cm)
483 }
484 }
485 commandsEnd = i + 1
486 T.Ln("commandStart", commandsStart, commandsEnd, args[commandsStart:commandsEnd])
487 T.Ln("found command", cm.Name, "argument number", i, "at depth", depth, "distance", dist)
488 commands[depth] = *cm
489 } else {
490 // commandsStart=i+1
491 // commandsEnd=i+1
492 // T.Ln("not found:", args[i], "commandStart", commandsStart, commandsEnd, args[commandsStart:commandsEnd])
493 T.Ln("argument", args[i], "is not a command", commandsStart, commandsEnd)
494 c.FoundArgs = append(c.FoundArgs, args[i])
495 }
496 }
497 // I.S(commands, cm)
498 // commandsEnd++
499 cmds := []int{}
500 if len(commands) == 0 {
501 I.Ln("setting default command")
502 commands[0] = c.Commands[0]
503 // I.S(commands[0])
504 ibs := commands[0]
505 cm = &ibs
506 log.AppColorizer = commands[0].Colorizer
507 log.App = commands[0].AppText
508 } else {
509 T.Ln("checking commands")
510 // I.S(commands)
511 for i := range commands {
512 cmds = append(cmds, i)
513 }
514 I.S(cmds)
515 if len(cmds) > 0 {
516 sort.Ints(cmds)
517 var cms []string
518 for i := range commands {
519 cms = append(cms, commands[i].Name)
520 }
521 if cmds[0] != 1 {
522 e = fmt.Errorf("commands must include base level item for disambiguation %v", cms)
523 }
524 prev := cmds[0]
525 for i := range cmds {
526 if i == 0 {
527 continue
528 }
529 if cmds[i] != prev+1 {
530 e = fmt.Errorf("more than one command specified, %v", cms)
531 return
532 }
533 found = false
534 for j := range commands[cmds[i-1]].Commands {
535 if commands[cmds[i]].Name == commands[cmds[i-1]].Commands[j].Name {
536 found = true
537 }
538 }
539 if !found {
540 e = fmt.Errorf("multiple commands are not a path on the command tree %v", cms)
541 return
542 }
543 }
544 }
545 T.Ln("commands:", commandsStart, commandsEnd)
546 T.Ln("length of gathered commands", len(commands))
547 if len(commands) == 1 {
548 for _, x := range commands {
549 cm = &x
550 if cm.Colorizer != nil {
551 log.AppColorizer = cm.Colorizer
552 log.App = cm.AppText
553 }
554 }
555 }
556 }
557 // if there was no command the commands start and end after all the args
558 if commandsStart < 0 || commandsEnd < 0 {
559 commandsStart = len(args)
560 commandsEnd = commandsStart
561 }
562 T.Ln("commands section:", commandsStart, commandsEnd)
563 if commandsStart > 0 {
564 T.Ln("opts found", args[:commandsStart])
565 // we have opts to check
566 for i := range args {
567 // if i == 0 {
568 // continue
569 // }
570 if i >= commandsStart {
571 break
572 }
573 var val string
574 var o opt.Option
575 if o, val, e = c.GetOption(args[i]); E.Chk(e) {
576 e = fmt.Errorf("argument %d: '%s' lacks a valid opt prefix", i, args[i])
577 return
578 }
579 if _, e = o.ReadInput(val); E.Chk(e) {
580 return
581 }
582 T.Ln("found opt:", o.String())
583 op = append(op, o)
584 optVals = append(optVals, val)
585 }
586 }
587 T.Ln("options to pass to child processes to match config:", args[:commandsStart])
588 589 if len(cmds) < 1 {
590 cmds = []int{0}
591 commands[0] = c.Commands[0]
592 }
593 if commandsEnd > 0 && len(args) > commandsEnd {
594 remArgs = args[commandsEnd:]
595 }
596 D.F("args that will pass to command: %v", remArgs)
597 // I.S(commands, cm)
598 // I.S(commands[cmds[len(cmds)-1]], op, args[commandsEnd:])
599 return
600 }
601 602 // ReadCAFile reads in the configured Certificate Authority for TLS connections
603 func (c *Config) ReadCAFile() []byte {
604 // Read certificate file if TLS is not disabled.
605 var certs []byte
606 if c.ClientTLS.True() {
607 var e error
608 if certs, e = ioutil.ReadFile(c.CAFile.V()); E.Chk(e) {
609 // If there's an error reading the CA file, continue with nil certs and without the client connection.
610 certs = nil
611 }
612 } else {
613 I.Ln("chain server RPC TLS is disabled")
614 }
615 return certs
616 }
617 618 func ifcToStrings(ifc []interface{}) (o []string) {
619 for i := range ifc {
620 o = append(o, ifc[i].(string))
621 }
622 return
623 }
624 625 type details struct {
626 name, option, desc, def string
627 aliases []string
628 documentation string
629 }
630 631 // GetHelp walks the command tree and gathers the options and creates a set of help functions for all commands and
632 // options in the set
633 func (c *Config) GetHelp(hf func(ifc interface{}) error) {
634 helpCommand := cmds.Command{
635 Name: "help",
636 Title: "prints information about how to use pod",
637 Entrypoint: hf,
638 Commands: nil,
639 }
640 // first add all the options
641 c.ForEach(func(ifc opt.Option) bool {
642 o := fmt.Sprintf("Parallelcoin Pod All-in-One Suite\n\n")
643 var dt details
644 switch ii := ifc.(type) {
645 case *binary.Opt:
646 dt = details{
647 ii.GetMetadata().Name, ii.Option, ii.Description,
648 fmt.Sprint(ii.Def), ii.Aliases,
649 ii.Documentation,
650 }
651 case *list.Opt:
652 dt = details{
653 ii.GetMetadata().Name, ii.Option, ii.Description,
654 fmt.Sprint(ii.Def), ii.Aliases,
655 ii.Documentation,
656 }
657 case *float.Opt:
658 dt = details{
659 ii.GetMetadata().Name, ii.Option, ii.Description,
660 fmt.Sprint(ii.Def), ii.Aliases,
661 ii.Documentation,
662 }
663 case *integer.Opt:
664 dt = details{
665 ii.GetMetadata().Name, ii.Option, ii.Description,
666 fmt.Sprint(ii.Def), ii.Aliases,
667 ii.Documentation,
668 }
669 case *text.Opt:
670 dt = details{
671 ii.GetMetadata().Name, ii.Option, ii.Description,
672 fmt.Sprint(ii.Def), ii.Aliases,
673 ii.Documentation,
674 }
675 case *duration.Opt:
676 dt = details{
677 ii.GetMetadata().Name, ii.Option, ii.Description,
678 fmt.Sprint(ii.Def), ii.Aliases,
679 ii.Documentation,
680 }
681 }
682 allNames := append([]string{dt.option}, dt.aliases...)
683 for i := range allNames {
684 helpCommand.Commands = append(helpCommand.Commands, cmds.Command{
685 Name: allNames[i],
686 Title: dt.desc,
687 Entrypoint: func(ifc interface{}) (e error) {
688 o += fmt.Sprintf(
689 "Help information about %s\n\n"+
690 "\toption name:\n\t\t%s\n"+
691 "\taliases:\n\t\t%s\n"+
692 "\tdescription:\n\t\t%s\n"+
693 "\tdefault:\n\t\t%v\n",
694 dt.name, dt.option, dt.aliases, dt.desc, dt.def,
695 )
696 if dt.documentation != "" {
697 o += "\tdocumentation:\n\t\t" + dt.documentation + "\n\n"
698 }
699 fmt.Fprint(os.Stderr, o)
700 return
701 },
702 Commands: nil,
703 Parent: &helpCommand,
704 },
705 )
706 }
707 // for i := range
708 return true
709 },
710 )
711 // next add all the commands
712 c.Commands.ForEach(func(cm cmds.Command) bool {
713 helpCommand.Commands = append(helpCommand.Commands, cmds.Command{
714 Name: cm.Name,
715 Title: cm.Title,
716 Description: cm.Description,
717 Entrypoint: func(ifc interface{}) (e error) {
718 o := fmt.Sprintf(
719 "Help information about command '%s'\n\n"+
720 "%s\n\n",
721 cm.Name, cm.Title,
722 )
723 if cm.Description != "" {
724 split := strings.Split(cm.Description, "\n")
725 docs := "\t" + strings.Join(split, "\n\n\t")
726 o += docs + "\n\n"
727 }
728 o += "Related options:\n\n"
729 descs := make(map[string]string)
730 c.ForEach(func(ifc opt.Option) bool {
731 meta := ifc.GetMetadata()
732 found := false
733 for i := range meta.Tags {
734 if meta.Tags[i] == cm.Name {
735 found = true
736 }
737 }
738 if !found {
739 // skip item as it isn't tagged with this program name
740 return true
741 }
742 oo := fmt.Sprintf("\t%s %v", meta.Option, meta.Aliases)
743 nrunes := utf8.RuneCountInString(oo)
744 var def string
745 switch ii := ifc.(type) {
746 case *binary.Opt:
747 def = fmt.Sprint(ii.Def)
748 case *list.Opt:
749 def = fmt.Sprint(ii.Def)
750 case *float.Opt:
751 def = fmt.Sprint(ii.Def)
752 case *integer.Opt:
753 def = fmt.Sprint(ii.Def)
754 case *text.Opt:
755 def = fmt.Sprint(ii.Def)
756 case *duration.Opt:
757 def = fmt.Sprint(ii.Def)
758 }
759 descs[meta.Group] += oo + fmt.Sprintf(strings.Repeat(" ", 32-nrunes)+"%s, default: %s\n", meta.Description, def)
760 return true
761 },
762 )
763 var cats []string
764 for i := range descs {
765 cats = append(cats, i)
766 }
767 // I.S(cats)
768 sort.Strings(cats)
769 for i := range cats {
770 if cats[i] != "" {
771 o += "\n" + cats[i] + "\n"
772 }
773 o += descs[cats[i]]
774 }
775 fmt.Fprint(os.Stderr, o)
776 return
777 },
778 Parent: &helpCommand,
779 })
780 return true
781 }, 0, 0,
782 )
783 c.Commands = append(c.Commands, helpCommand)
784 return
785 }
786 787 func getAllOptionStrings(c *Config) (s map[string][]string, e error) {
788 s = make(map[string][]string)
789 if c.ForEach(func(ifc opt.Option) bool {
790 md := ifc.GetMetadata()
791 if _, ok := s[ifc.Name()]; ok {
792 e = fmt.Errorf("conflicting opt names: %v %v", ifc.GetAllOptionStrings(), s[ifc.Name()])
793 return false
794 }
795 s[ifc.Name()] = md.GetAllOptionStrings()
796 return true
797 },
798 ) {
799 }
800 // s["commandslist"] = c.Commands.GetAllCommands()
801 return
802 }
803 804 func findConflictingItems(valOpts map[string][]string) (o []string, e error) {
805 var ss, ls string
806 for i := range valOpts {
807 for j := range valOpts {
808 if i == j {
809 continue
810 }
811 a := valOpts[i]
812 b := valOpts[j]
813 for ii := range a {
814 for jj := range b {
815 ss, ls = shortestString(a[ii], b[jj])
816 if ss == ls[:len(ss)] {
817 E.F("conflict between %s and %s, ", ss, ls)
818 o = append(o, ss, ls)
819 }
820 }
821 }
822 }
823 }
824 if len(o) > 0 {
825 panic(fmt.Sprintf("conflicts found: %v", o))
826 }
827 return
828 }
829 830 func shortestString(a, b string) (s, l string) {
831 switch {
832 case len(a) > len(b):
833 s, l = b, a
834 default:
835 s, l = a, b
836 }
837 return
838 }
839