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.

189 lines
5.7 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. data "github.com/tendermint/go-wire/data"
  10. "github.com/tendermint/go-wire/data/base58"
  11. )
  12. const (
  13. RootFlag = "root"
  14. HomeFlag = "home"
  15. TraceFlag = "trace"
  16. OutputFlag = "output"
  17. EncodingFlag = "encoding"
  18. )
  19. // Executable is the minimal interface to *corba.Command, so we can
  20. // wrap if desired before the test
  21. type Executable interface {
  22. Execute() error
  23. }
  24. // PrepareBaseCmd is meant for tendermint and other servers
  25. func PrepareBaseCmd(cmd *cobra.Command, envPrefix, defautRoot string) Executor {
  26. cobra.OnInitialize(func() { initEnv(envPrefix) })
  27. cmd.PersistentFlags().StringP(RootFlag, "r", defautRoot, "DEPRECATED. Use --home")
  28. // -h is already reserved for --help as part of the cobra framework
  29. // do you want to try something else??
  30. // also, default must be empty, so we can detect this unset and fall back
  31. // to --root / TM_ROOT / TMROOT
  32. cmd.PersistentFlags().String(HomeFlag, "", "root directory for config and data")
  33. cmd.PersistentFlags().Bool(TraceFlag, false, "print out full stack trace on errors")
  34. cmd.PersistentPreRunE = concatCobraCmdFuncs(bindFlagsLoadViper, cmd.PersistentPreRunE)
  35. return Executor{cmd, os.Exit}
  36. }
  37. // PrepareMainCmd is meant for client side libs that want some more flags
  38. //
  39. // This adds --encoding (hex, btc, base64) and --output (text, json) to
  40. // the command. These only really make sense in interactive commands.
  41. func PrepareMainCmd(cmd *cobra.Command, envPrefix, defautRoot string) Executor {
  42. cmd.PersistentFlags().StringP(EncodingFlag, "e", "hex", "Binary encoding (hex|b64|btc)")
  43. cmd.PersistentFlags().StringP(OutputFlag, "o", "text", "Output format (text|json)")
  44. cmd.PersistentPreRunE = concatCobraCmdFuncs(setEncoding, validateOutput, cmd.PersistentPreRunE)
  45. return PrepareBaseCmd(cmd, envPrefix, defautRoot)
  46. }
  47. // initEnv sets to use ENV variables if set.
  48. func initEnv(prefix string) {
  49. copyEnvVars(prefix)
  50. // env variables with TM prefix (eg. TM_ROOT)
  51. viper.SetEnvPrefix(prefix)
  52. viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
  53. viper.AutomaticEnv()
  54. }
  55. // This copies all variables like TMROOT to TM_ROOT,
  56. // so we can support both formats for the user
  57. func copyEnvVars(prefix string) {
  58. prefix = strings.ToUpper(prefix)
  59. ps := prefix + "_"
  60. for _, e := range os.Environ() {
  61. kv := strings.SplitN(e, "=", 2)
  62. if len(kv) == 2 {
  63. k, v := kv[0], kv[1]
  64. if strings.HasPrefix(k, prefix) && !strings.HasPrefix(k, ps) {
  65. k2 := strings.Replace(k, prefix, ps, 1)
  66. os.Setenv(k2, v)
  67. }
  68. }
  69. }
  70. }
  71. // Executor wraps the cobra Command with a nicer Execute method
  72. type Executor struct {
  73. *cobra.Command
  74. Exit func(int) // this is os.Exit by default, override in tests
  75. }
  76. type ExitCoder interface {
  77. ExitCode() int
  78. }
  79. // execute adds all child commands to the root command sets flags appropriately.
  80. // This is called by main.main(). It only needs to happen once to the rootCmd.
  81. func (e Executor) Execute() error {
  82. e.SilenceUsage = true
  83. e.SilenceErrors = true
  84. err := e.Command.Execute()
  85. if err != nil {
  86. if viper.GetBool(TraceFlag) {
  87. fmt.Fprintf(os.Stderr, "ERROR: %+v\n", err)
  88. } else {
  89. fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
  90. }
  91. // return error code 1 by default, can override it with a special error type
  92. exitCode := 1
  93. if ec, ok := err.(ExitCoder); ok {
  94. exitCode = ec.ExitCode()
  95. }
  96. e.Exit(exitCode)
  97. }
  98. return err
  99. }
  100. type cobraCmdFunc func(cmd *cobra.Command, args []string) error
  101. // Returns a single function that calls each argument function in sequence
  102. // RunE, PreRunE, PersistentPreRunE, etc. all have this same signature
  103. func concatCobraCmdFuncs(fs ...cobraCmdFunc) cobraCmdFunc {
  104. return func(cmd *cobra.Command, args []string) error {
  105. for _, f := range fs {
  106. if f != nil {
  107. if err := f(cmd, args); err != nil {
  108. return err
  109. }
  110. }
  111. }
  112. return nil
  113. }
  114. }
  115. // Bind all flags and read the config into viper
  116. func bindFlagsLoadViper(cmd *cobra.Command, args []string) error {
  117. // cmd.Flags() includes flags from this command and all persistent flags from the parent
  118. if err := viper.BindPFlags(cmd.Flags()); err != nil {
  119. return err
  120. }
  121. // rootDir is command line flag, env variable, or default $HOME/.tlc
  122. // NOTE: we support both --root and --home for now, but eventually only --home
  123. // Also ensure we set the correct rootDir under HomeFlag so we dont need to
  124. // repeat this logic elsewhere.
  125. rootDir := viper.GetString(HomeFlag)
  126. if rootDir == "" {
  127. rootDir = viper.GetString(RootFlag)
  128. viper.Set(HomeFlag, rootDir)
  129. }
  130. viper.SetConfigName("config") // name of config file (without extension)
  131. viper.AddConfigPath(rootDir) // search root directory
  132. // If a config file is found, read it in.
  133. if err := viper.ReadInConfig(); err == nil {
  134. // stderr, so if we redirect output to json file, this doesn't appear
  135. // fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
  136. } else if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
  137. // we ignore not found error, only parse error
  138. // stderr, so if we redirect output to json file, this doesn't appear
  139. fmt.Fprintf(os.Stderr, "%#v", err)
  140. }
  141. return nil
  142. }
  143. // setEncoding reads the encoding flag
  144. func setEncoding(cmd *cobra.Command, args []string) error {
  145. // validate and set encoding
  146. enc := viper.GetString("encoding")
  147. switch enc {
  148. case "hex":
  149. data.Encoder = data.HexEncoder
  150. case "b64":
  151. data.Encoder = data.B64Encoder
  152. case "btc":
  153. data.Encoder = base58.BTCEncoder
  154. default:
  155. return errors.Errorf("Unsupported encoding: %s", enc)
  156. }
  157. return nil
  158. }
  159. func validateOutput(cmd *cobra.Command, args []string) error {
  160. // validate output format
  161. output := viper.GetString(OutputFlag)
  162. switch output {
  163. case "text", "json":
  164. default:
  165. return errors.Errorf("Unsupported output format: %s", output)
  166. }
  167. return nil
  168. }