You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

156 lines
4.6 KiB

  1. package cli
  2. import (
  3. "fmt"
  4. "os"
  5. "strings"
  6. "github.com/pkg/errors"
  7. "github.com/spf13/cobra"
  8. "github.com/spf13/viper"
  9. )
  10. const (
  11. HomeFlag = "home"
  12. TraceFlag = "trace"
  13. OutputFlag = "output"
  14. EncodingFlag = "encoding"
  15. )
  16. // Executable is the minimal interface to *corba.Command, so we can
  17. // wrap if desired before the test
  18. type Executable interface {
  19. Execute() error
  20. }
  21. // PrepareBaseCmd is meant for tendermint and other servers
  22. func PrepareBaseCmd(cmd *cobra.Command, envPrefix, defaultHome string) Executor {
  23. cobra.OnInitialize(func() { initEnv(envPrefix) })
  24. cmd.PersistentFlags().StringP(HomeFlag, "", defaultHome, "directory for config and data")
  25. cmd.PersistentFlags().Bool(TraceFlag, false, "print out full stack trace on errors")
  26. cmd.PersistentPreRunE = concatCobraCmdFuncs(bindFlagsLoadViper, cmd.PersistentPreRunE)
  27. return Executor{cmd, os.Exit}
  28. }
  29. // PrepareMainCmd is meant for client side libs that want some more flags
  30. //
  31. // This adds --encoding (hex, btc, base64) and --output (text, json) to
  32. // the command. These only really make sense in interactive commands.
  33. func PrepareMainCmd(cmd *cobra.Command, envPrefix, defaultHome string) Executor {
  34. cmd.PersistentFlags().StringP(EncodingFlag, "e", "hex", "Binary encoding (hex|b64|btc)")
  35. cmd.PersistentFlags().StringP(OutputFlag, "o", "text", "Output format (text|json)")
  36. cmd.PersistentPreRunE = concatCobraCmdFuncs(validateOutput, cmd.PersistentPreRunE)
  37. return PrepareBaseCmd(cmd, envPrefix, defaultHome)
  38. }
  39. // initEnv sets to use ENV variables if set.
  40. func initEnv(prefix string) {
  41. copyEnvVars(prefix)
  42. // env variables with TM prefix (eg. TM_ROOT)
  43. viper.SetEnvPrefix(prefix)
  44. viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
  45. viper.AutomaticEnv()
  46. }
  47. // This copies all variables like TMROOT to TM_ROOT,
  48. // so we can support both formats for the user
  49. func copyEnvVars(prefix string) {
  50. prefix = strings.ToUpper(prefix)
  51. ps := prefix + "_"
  52. for _, e := range os.Environ() {
  53. kv := strings.SplitN(e, "=", 2)
  54. if len(kv) == 2 {
  55. k, v := kv[0], kv[1]
  56. if strings.HasPrefix(k, prefix) && !strings.HasPrefix(k, ps) {
  57. k2 := strings.Replace(k, prefix, ps, 1)
  58. os.Setenv(k2, v)
  59. }
  60. }
  61. }
  62. }
  63. // Executor wraps the cobra Command with a nicer Execute method
  64. type Executor struct {
  65. *cobra.Command
  66. Exit func(int) // this is os.Exit by default, override in tests
  67. }
  68. type ExitCoder interface {
  69. ExitCode() int
  70. }
  71. // execute adds all child commands to the root command sets flags appropriately.
  72. // This is called by main.main(). It only needs to happen once to the rootCmd.
  73. func (e Executor) Execute() error {
  74. e.SilenceUsage = true
  75. e.SilenceErrors = true
  76. err := e.Command.Execute()
  77. if err != nil {
  78. if viper.GetBool(TraceFlag) {
  79. fmt.Fprintf(os.Stderr, "ERROR: %+v\n", err)
  80. } else {
  81. fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
  82. }
  83. // return error code 1 by default, can override it with a special error type
  84. exitCode := 1
  85. if ec, ok := err.(ExitCoder); ok {
  86. exitCode = ec.ExitCode()
  87. }
  88. e.Exit(exitCode)
  89. }
  90. return err
  91. }
  92. type cobraCmdFunc func(cmd *cobra.Command, args []string) error
  93. // Returns a single function that calls each argument function in sequence
  94. // RunE, PreRunE, PersistentPreRunE, etc. all have this same signature
  95. func concatCobraCmdFuncs(fs ...cobraCmdFunc) cobraCmdFunc {
  96. return func(cmd *cobra.Command, args []string) error {
  97. for _, f := range fs {
  98. if f != nil {
  99. if err := f(cmd, args); err != nil {
  100. return err
  101. }
  102. }
  103. }
  104. return nil
  105. }
  106. }
  107. // Bind all flags and read the config into viper
  108. func bindFlagsLoadViper(cmd *cobra.Command, args []string) error {
  109. // cmd.Flags() includes flags from this command and all persistent flags from the parent
  110. if err := viper.BindPFlags(cmd.Flags()); err != nil {
  111. return err
  112. }
  113. homeDir := viper.GetString(HomeFlag)
  114. viper.Set(HomeFlag, homeDir)
  115. viper.SetConfigName("config") // name of config file (without extension)
  116. viper.AddConfigPath(homeDir) // search root directory
  117. // If a config file is found, read it in.
  118. if err := viper.ReadInConfig(); err == nil {
  119. // stderr, so if we redirect output to json file, this doesn't appear
  120. // fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
  121. } else if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
  122. // we ignore not found error, only parse error
  123. // stderr, so if we redirect output to json file, this doesn't appear
  124. fmt.Fprintf(os.Stderr, "%#v", err)
  125. }
  126. return nil
  127. }
  128. func validateOutput(cmd *cobra.Command, args []string) error {
  129. // validate output format
  130. output := viper.GetString(OutputFlag)
  131. switch output {
  132. case "text", "json":
  133. default:
  134. return errors.Errorf("Unsupported output format: %s", output)
  135. }
  136. return nil
  137. }