1 // Copyright (c) 2015 The btcsuite developers
2 // Use of this source code is governed by an ISC
3 // license that can be found in the LICENSE file.
15 // baseHelpDescs house the various help labels, types, and example values used
16 // when generating help. The per-command synopsis, field descriptions,
17 // conditions, and result descriptions are to be provided by the caller.
18 var baseHelpDescs = map[string]string{
19 // Misc help labels and output.
20 "help-arguments": "Arguments",
21 "help-arguments-none": "None",
22 "help-result": "Result",
23 "help-result-nothing": "Nothing",
24 "help-default": "default",
25 "help-optional": "optional",
26 "help-required": "required",
29 "json-type-numeric": "numeric",
30 "json-type-string": "string",
31 "json-type-bool": "boolean",
32 "json-type-array": "array of ",
33 "json-type-object": "object",
34 "json-type-value": "value",
37 "json-example-string": "value",
38 "json-example-bool": "true|false",
39 "json-example-map-data": "data",
40 "json-example-unknown": "unknown",
43 // descLookupFunc is a function which is used to lookup a description given
45 type descLookupFunc func(string) string
47 // reflectTypeToJSONType returns a string that represents the JSON type
48 // associated with the provided Go type.
49 func reflectTypeToJSONType(xT descLookupFunc, rt reflect.Type) string {
52 return xT("json-type-numeric")
57 return xT("json-type-string")
60 return xT("json-type-bool")
62 case reflect.Array, reflect.Slice:
63 return xT("json-type-array") + reflectTypeToJSONType(xT,
67 return xT("json-type-object")
70 return xT("json-type-object")
73 return xT("json-type-value")
76 // resultStructHelp returns a slice of strings containing the result help output
77 // for a struct. Each line makes use of tabs to separate the relevant pieces so
78 // a tabwriter can be used later to line everything up. The descriptions are
79 // pulled from the active help descriptions map based on the lowercase version
80 // of the provided reflect type and json name (or the lowercase version of the
81 // field name if no json tag was specified).
82 func resultStructHelp(xT descLookupFunc, rt reflect.Type, indentLevel int) []string {
83 indent := strings.Repeat(" ", indentLevel)
84 typeName := strings.ToLower(rt.Name())
86 // Generate the help for each of the fields in the result struct.
87 numField := rt.NumField()
88 results := make([]string, 0, numField)
89 for i := 0; i < numField; i++ {
92 // The field name to display is the json name when it's
93 // available, otherwise use the lowercase field name.
95 if tag := rtf.Tag.Get("json"); tag != "" {
96 fieldName = strings.Split(tag, ",")[0]
98 fieldName = strings.ToLower(rtf.Name)
101 // Deference pointer if needed.
103 if rtfType.Kind() == reflect.Ptr {
104 rtfType = rtf.Type.Elem()
107 // Generate the JSON example for the result type of this struct
108 // field. When it is a complex type, examine the type and
109 // adjust the opening bracket and brace combination accordingly.
110 fieldType := reflectTypeToJSONType(xT, rtfType)
111 fieldDescKey := typeName + "-" + fieldName
112 fieldExamples, isComplex := reflectTypeToJSONExample(xT,
113 rtfType, indentLevel, fieldDescKey)
116 kind := rtfType.Kind()
117 if kind == reflect.Array || kind == reflect.Slice {
122 result := fmt.Sprintf("%s\"%s\": %s\t(%s)\t%s", indent,
123 fieldName, brace, fieldType, xT(fieldDescKey))
124 results = append(results, result)
125 results = append(results, fieldExamples...)
127 result := fmt.Sprintf("%s\"%s\": %s,\t(%s)\t%s", indent,
128 fieldName, fieldExamples[0], fieldType,
130 results = append(results, result)
137 // reflectTypeToJSONExample generates example usage in the format used by the
138 // help output. It handles arrays, slices and structs recursively. The output
139 // is returned as a slice of lines so the final help can be nicely aligned via
140 // a tab writer. A bool is also returned which specifies whether or not the
141 // type results in a complex JSON object since they need to be handled
143 func reflectTypeToJSONExample(xT descLookupFunc, rt reflect.Type, indentLevel int, fieldDescKey string) ([]string, bool) {
144 // Indirect pointer if needed.
145 if rt.Kind() == reflect.Ptr {
150 if kind == reflect.Float32 || kind == reflect.Float64 {
151 return []string{"n.nnn"}, false
154 return []string{"n"}, false
159 return []string{`"` + xT("json-example-string") + `"`}, false
162 return []string{xT("json-example-bool")}, false
165 indent := strings.Repeat(" ", indentLevel)
166 results := resultStructHelp(xT, rt, indentLevel+1)
168 // An opening brace is needed for the first indent level. For
169 // all others, it will be included as a part of the previous
171 if indentLevel == 0 {
172 newResults := make([]string, len(results)+1)
174 copy(newResults[1:], results)
178 // The closing brace has a comma after it except for the first
179 // indent level. The final tabs are necessary so the tab writer
180 // lines things up properly.
181 closingBrace := indent + "}"
185 results = append(results, closingBrace+"\t\t")
188 case reflect.Array, reflect.Slice:
189 results, isComplex := reflectTypeToJSONExample(xT, rt.Elem(),
190 indentLevel, fieldDescKey)
192 // When the result is complex, it is because this is an array of
195 // When this is at indent level zero, there is no
196 // previous field to house the opening array bracket, so
197 // replace the opening object brace with the array
198 // syntax. Also, replace the final closing object brace
199 // with the variadiac array closing syntax.
200 indent := strings.Repeat(" ", indentLevel)
201 if indentLevel == 0 {
202 results[0] = indent + "[{"
203 results[len(results)-1] = indent + "},...]"
207 // At this point, the indent level is greater than 0, so
208 // the opening array bracket and object brace are
209 // already a part of the previous field. However, the
210 // closing entry is a simple object brace, so replace it
211 // with the variadiac array closing syntax. The final
212 // tabs are necessary so the tab writer lines things up
214 results[len(results)-1] = indent + "},...],\t\t"
218 // It's an array of primitives, so return the formatted text
220 return []string{fmt.Sprintf("[%s,...]", results[0])}, false
223 indent := strings.Repeat(" ", indentLevel)
224 results := make([]string, 0, 3)
226 // An opening brace is needed for the first indent level. For
227 // all others, it will be included as a part of the previous
229 if indentLevel == 0 {
230 results = append(results, indent+"{")
233 // Maps are a bit special in that they need to have the key,
234 // value, and description of the object entry specifically
236 innerIndent := strings.Repeat(" ", indentLevel+1)
237 result := fmt.Sprintf("%s%q: %s, (%s) %s", innerIndent,
238 xT(fieldDescKey+"--key"), xT(fieldDescKey+"--value"),
239 reflectTypeToJSONType(xT, rt), xT(fieldDescKey+"--desc"))
240 results = append(results, result)
241 results = append(results, innerIndent+"...")
243 results = append(results, indent+"}")
247 return []string{xT("json-example-unknown")}, false
250 // resultTypeHelp generates and returns formatted help for the provided result
252 func resultTypeHelp(xT descLookupFunc, rt reflect.Type, fieldDescKey string) string {
253 // Generate the JSON example for the result type.
254 results, isComplex := reflectTypeToJSONExample(xT, rt, 0, fieldDescKey)
256 // When this is a primitive type, add the associated JSON type and
257 // result description into the final string, format it accordingly,
260 return fmt.Sprintf("%s (%s) %s", results[0],
261 reflectTypeToJSONType(xT, rt), xT(fieldDescKey))
264 // At this point, this is a complex type that already has the JSON types
265 // and descriptions in the results. Thus, use a tab writer to nicely
266 // align the help text.
267 var formatted bytes.Buffer
268 w := new(tabwriter.Writer)
269 w.Init(&formatted, 0, 4, 1, ' ', 0)
270 for i, text := range results {
271 if i == len(results)-1 {
274 fmt.Fprintln(w, text)
278 return formatted.String()
281 // argTypeHelp returns the type of provided command argument as a string in the
282 // format used by the help output. In particular, it includes the JSON type
283 // (boolean, numeric, string, array, object) along with optional and the default
284 // value if applicable.
285 func argTypeHelp(xT descLookupFunc, structField reflect.StructField, defaultVal *reflect.Value) string {
286 // Indirect the pointer if needed and track if it's an optional field.
287 fieldType := structField.Type
289 if fieldType.Kind() == reflect.Ptr {
290 fieldType = fieldType.Elem()
294 // When there is a default value, it must also be a pointer due to the
295 // rules enforced by RegisterCmd.
296 if defaultVal != nil {
297 indirect := defaultVal.Elem()
298 defaultVal = &indirect
301 // Convert the field type to a JSON type.
302 details := make([]string, 0, 3)
303 details = append(details, reflectTypeToJSONType(xT, fieldType))
305 // Add optional and default value to the details if needed.
307 details = append(details, xT("help-optional"))
309 // Add the default value if there is one. This is only checked
310 // when the field is optional since a non-optional field can't
311 // have a default value.
312 if defaultVal != nil {
313 val := defaultVal.Interface()
314 if defaultVal.Kind() == reflect.String {
315 val = fmt.Sprintf(`"%s"`, val)
317 str := fmt.Sprintf("%s=%v", xT("help-default"), val)
318 details = append(details, str)
321 details = append(details, xT("help-required"))
324 return strings.Join(details, ", ")
327 // argHelp generates and returns formatted help for the provided command.
328 func argHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string) string {
329 // Return now if the command has no arguments.
331 numFields := rt.NumField()
336 // Generate the help for each argument in the command. Several
337 // simplifying assumptions are made here because the RegisterCmd
338 // function has already rigorously enforced the layout.
339 args := make([]string, 0, numFields)
340 for i := 0; i < numFields; i++ {
342 var defaultVal *reflect.Value
343 if defVal, ok := defaults[i]; ok {
347 fieldName := strings.ToLower(rtf.Name)
348 helpText := fmt.Sprintf("%d.\t%s\t(%s)\t%s", i+1, fieldName,
349 argTypeHelp(xT, rtf, defaultVal),
350 xT(method+"-"+fieldName))
351 args = append(args, helpText)
353 // For types which require a JSON object, or an array of JSON
354 // objects, generate the full syntax for the argument.
355 fieldType := rtf.Type
356 if fieldType.Kind() == reflect.Ptr {
357 fieldType = fieldType.Elem()
359 kind := fieldType.Kind()
362 fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
363 resultText := resultTypeHelp(xT, fieldType, fieldDescKey)
364 args = append(args, resultText)
367 fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
368 resultText := resultTypeHelp(xT, fieldType, fieldDescKey)
369 args = append(args, resultText)
371 case reflect.Array, reflect.Slice:
372 fieldDescKey := fmt.Sprintf("%s-%s", method, fieldName)
373 if rtf.Type.Elem().Kind() == reflect.Struct {
374 resultText := resultTypeHelp(xT, fieldType,
376 args = append(args, resultText)
381 // Add argument names, types, and descriptions if there are any. Use a
382 // tab writer to nicely align the help text.
383 var formatted bytes.Buffer
384 w := new(tabwriter.Writer)
385 w.Init(&formatted, 0, 4, 1, ' ', 0)
386 for _, text := range args {
387 fmt.Fprintln(w, text)
390 return formatted.String()
393 // methodHelp generates and returns the help output for the provided command
394 // and method info. This is the main work horse for the exported MethodHelp
396 func methodHelp(xT descLookupFunc, rtp reflect.Type, defaults map[int]reflect.Value, method string, resultTypes []interface{}) string {
397 // Start off with the method usage and help synopsis.
398 help := fmt.Sprintf("%s\n\n%s\n", methodUsageText(rtp, defaults, method),
399 xT(method+"--synopsis"))
401 // Generate the help for each argument in the command.
402 if argText := argHelp(xT, rtp, defaults, method); argText != "" {
403 help += fmt.Sprintf("\n%s:\n%s", xT("help-arguments"),
406 help += fmt.Sprintf("\n%s:\n%s\n", xT("help-arguments"),
407 xT("help-arguments-none"))
410 // Generate the help text for each result type.
411 resultTexts := make([]string, 0, len(resultTypes))
412 for i := range resultTypes {
413 rtp := reflect.TypeOf(resultTypes[i])
414 fieldDescKey := fmt.Sprintf("%s--result%d", method, i)
415 if resultTypes[i] == nil {
416 resultText := xT("help-result-nothing")
417 resultTexts = append(resultTexts, resultText)
421 resultText := resultTypeHelp(xT, rtp.Elem(), fieldDescKey)
422 resultTexts = append(resultTexts, resultText)
425 // Add result types and descriptions. When there is more than one
426 // result type, also add the condition which triggers it.
427 if len(resultTexts) > 1 {
428 for i, resultText := range resultTexts {
429 condKey := fmt.Sprintf("%s--condition%d", method, i)
430 help += fmt.Sprintf("\n%s (%s):\n%s\n",
431 xT("help-result"), xT(condKey), resultText)
433 } else if len(resultTexts) > 0 {
434 help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"),
437 help += fmt.Sprintf("\n%s:\n%s\n", xT("help-result"),
438 xT("help-result-nothing"))
443 // isValidResultType returns whether the passed reflect kind is one of the
444 // acceptable types for results.
445 func isValidResultType(kind reflect.Kind) bool {
451 case reflect.String, reflect.Struct, reflect.Array, reflect.Slice,
452 reflect.Bool, reflect.Map:
460 // GenerateHelp generates and returns help output for the provided method and
461 // result types given a map to provide the appropriate keys for the method
462 // synopsis, field descriptions, conditions, and result descriptions. The
463 // method must be associated with a registered type. All commands provided by
464 // this package are registered by default.
466 // The resultTypes must be pointer-to-types which represent the specific types
467 // of values the command returns. For example, if the command only returns a
468 // boolean value, there should only be a single entry of (*bool)(nil). Note
469 // that each type must be a single pointer to the type. Therefore, it is
470 // recommended to simply pass a nil pointer cast to the appropriate type as
473 // The provided descriptions map must contain all of the keys or an error will
474 // be returned which includes the missing key, or the final missing key when
475 // there is more than one key missing. The generated help in the case of such
476 // an error will use the key in place of the description.
478 // The following outlines the required keys:
479 // "<method>--synopsis" Synopsis for the command
480 // "<method>-<lowerfieldname>" Description for each command argument
481 // "<typename>-<lowerfieldname>" Description for each object field
482 // "<method>--condition<#>" Description for each result condition
483 // "<method>--result<#>" Description for each primitive result num
485 // Notice that the "special" keys synopsis, condition<#>, and result<#> are
486 // preceded by a double dash to ensure they don't conflict with field names.
488 // The condition keys are only required when there is more than on result type,
489 // and the result key for a given result type is only required if it's not an
492 // For example, consider the 'help' command itself. There are two possible
493 // returns depending on the provided parameters. So, the help would be
494 // generated by calling the function as follows:
495 // GenerateHelp("help", descs, (*string)(nil), (*string)(nil)).
497 // The following keys would then be required in the provided descriptions map:
499 // "help--synopsis": "Returns a list of all commands or help for ...."
500 // "help-command": "The command to retrieve help for",
501 // "help--condition0": "no command provided"
502 // "help--condition1": "command specified"
503 // "help--result0": "List of commands"
504 // "help--result1": "Help for specified command"
505 func GenerateHelp(method string, descs map[string]string, resultTypes ...interface{}) (string, error) {
506 // Look up details about the provided method and error out if not
509 rtp, ok := methodToConcreteType[method]
510 info := methodToInfo[method]
511 registerLock.RUnlock()
513 str := fmt.Sprintf("%q is not registered", method)
514 return "", makeError(ErrUnregisteredMethod, str)
517 // Validate each result type is a pointer to a supported type (or nil).
518 for i, resultType := range resultTypes {
519 if resultType == nil {
523 rtp := reflect.TypeOf(resultType)
524 if rtp.Kind() != reflect.Ptr {
525 str := fmt.Sprintf("result #%d (%v) is not a pointer",
527 return "", makeError(ErrInvalidType, str)
530 elemKind := rtp.Elem().Kind()
531 if !isValidResultType(elemKind) {
532 str := fmt.Sprintf("result #%d (%v) is not an allowed "+
534 return "", makeError(ErrInvalidType, str)
538 // Create a closure for the description lookup function which falls back
539 // to the base help descriptions map for unrecognized keys and tracks
541 var missingKey string
542 xT := func(key string) string {
543 if desc, ok := descs[key]; ok {
546 if desc, ok := baseHelpDescs[key]; ok {
554 // Generate and return the help for the method.
555 help := methodHelp(xT, rtp, info.defaults, method, resultTypes)
556 if missingKey != "" {
557 return help, makeError(ErrMissingDescription, missingKey)