app.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. package app
  2. import (
  3. "fmt"
  4. "github.com/fatih/color"
  5. "github.com/moby/term"
  6. "github.com/spf13/cobra"
  7. "github.com/spf13/viper"
  8. "gogs.tyduyong.com/duyong/dy-pkg/app/flagsets"
  9. version2 "gogs.tyduyong.com/duyong/dy-pkg/app/version"
  10. "gogs.tyduyong.com/duyong/dy-pkg/logs"
  11. "io"
  12. "os"
  13. "runtime"
  14. "strings"
  15. )
  16. var progressMessage = color.GreenString("==>")
  17. type App struct {
  18. basename string
  19. name string
  20. description string
  21. options CommandLineOptions
  22. runFunc RunFunc
  23. silence bool
  24. noVersion bool
  25. noConfig bool
  26. commands []*Command
  27. args cobra.PositionalArgs
  28. cmd *cobra.Command
  29. }
  30. type RunFunc func(basename string) error
  31. type Option func(*App)
  32. func WithOptions(opt CommandLineOptions) Option {
  33. return func(a *App) {
  34. a.options = opt
  35. }
  36. }
  37. func WithRunFunc(run RunFunc) Option {
  38. return func(a *App) {
  39. a.runFunc = run
  40. }
  41. }
  42. func WithDescription(desc string) Option {
  43. return func(a *App) {
  44. a.description = desc
  45. }
  46. }
  47. func WithSilence() Option {
  48. return func(a *App) {
  49. a.silence = true
  50. }
  51. }
  52. func WithNoVersion() Option {
  53. return func(a *App) {
  54. a.noVersion = true
  55. }
  56. }
  57. func WithNoConfig() Option {
  58. return func(a *App) {
  59. a.noConfig = true
  60. }
  61. }
  62. func WithValidArgs(args cobra.PositionalArgs) Option {
  63. return func(a *App) {
  64. a.args = args
  65. }
  66. }
  67. func WithDefaultValidArgs() Option {
  68. return func(a *App) {
  69. a.args = func(cmd *cobra.Command, args []string) error {
  70. for _, arg := range args {
  71. if len(arg) > 0 {
  72. return fmt.Errorf("%q does not take any arguments, got %q", cmd.CommandPath(), args)
  73. }
  74. }
  75. return nil
  76. }
  77. }
  78. }
  79. func NewApp(name string, basename string, opts ...Option) *App {
  80. a := &App{
  81. name: name,
  82. basename: basename,
  83. }
  84. for _, o := range opts {
  85. o(a)
  86. }
  87. a.buildCommand()
  88. return a
  89. }
  90. func (a *App) buildCommand() {
  91. cmd := cobra.Command{
  92. Use: FormatBaseName(a.basename),
  93. Short: a.name,
  94. Long: a.description,
  95. // stop printing usage when the command errors
  96. SilenceUsage: true,
  97. SilenceErrors: true,
  98. Args: a.args,
  99. }
  100. // cmd.SetUsageTemplate(usageTemplate)
  101. cmd.SetOut(os.Stdout)
  102. cmd.SetErr(os.Stderr)
  103. cmd.Flags().SortFlags = true
  104. //cliflag.InitFlags(cmd.Flags())
  105. if len(a.commands) > 0 {
  106. for _, command := range a.commands {
  107. cmd.AddCommand(command.cobraCommand())
  108. }
  109. cmd.SetHelpCommand(helpCommand(FormatBaseName(a.basename)))
  110. }
  111. if a.runFunc != nil {
  112. cmd.RunE = a.runCommand
  113. }
  114. var namedFlagSets flagsets.NamedFlagSets
  115. if a.options != nil {
  116. namedFlagSets = a.options.Flags()
  117. fs := cmd.Flags()
  118. for _, f := range namedFlagSets.FlagSets {
  119. fs.AddFlagSet(f)
  120. }
  121. }
  122. if !a.noVersion {
  123. version2.AddFlags(namedFlagSets.FlagSet("global"))
  124. }
  125. if !a.noConfig {
  126. addConfigFlag(a.basename, namedFlagSets.FlagSet("global"))
  127. }
  128. cmd.Flags().AddFlagSet(namedFlagSets.FlagSet("global"))
  129. addCmdTemplate(&cmd, namedFlagSets)
  130. a.cmd = &cmd
  131. }
  132. func (a *App) runCommand(cmd *cobra.Command, args []string) error {
  133. if !a.noVersion {
  134. // display application version information
  135. version2.PrintAndExitIfRequested()
  136. }
  137. if !a.noConfig {
  138. if err := viper.BindPFlags(cmd.Flags()); err != nil {
  139. return err
  140. }
  141. if err := viper.Unmarshal(a.options); err != nil {
  142. return err
  143. }
  144. }
  145. if !a.silence {
  146. logs.Infof("%v Starting %s ...", progressMessage, a.name)
  147. if !a.noVersion {
  148. logs.Infof("%v Version: `%s`", progressMessage, version2.Get().ToJSON())
  149. }
  150. if !a.noConfig {
  151. logs.Infof("%v Config file used: `%s`", progressMessage, viper.ConfigFileUsed())
  152. }
  153. }
  154. if a.options != nil {
  155. if err := a.applyOptionRules(); err != nil {
  156. return err
  157. }
  158. }
  159. // run application
  160. if a.runFunc != nil {
  161. return a.runFunc(a.basename)
  162. }
  163. return nil
  164. }
  165. func (a *App) applyOptionRules() error {
  166. if completeableOptions, ok := a.options.(CompleteOptions); ok {
  167. if err := completeableOptions.Complete(); err != nil {
  168. return err
  169. }
  170. }
  171. if errs := a.options.Validate(); len(errs) != 0 {
  172. return errs[0]
  173. }
  174. if printableOptions, ok := a.options.(PrintOptions); ok && !a.silence {
  175. logs.Infof("%v Config: `%s`", progressMessage, printableOptions.String())
  176. }
  177. return nil
  178. }
  179. func (a *App) Run() {
  180. if err := a.cmd.Execute(); err != nil {
  181. fmt.Printf("%v %v\n", color.RedString("Error:"), err)
  182. os.Exit(1)
  183. }
  184. }
  185. func FormatBaseName(basename string) string {
  186. // Make case-insensitive and strip executable suffix if present
  187. if runtime.GOOS == "windows" {
  188. basename = strings.ToLower(basename)
  189. basename = strings.TrimSuffix(basename, ".exe")
  190. }
  191. return basename
  192. }
  193. func addCmdTemplate(cmd *cobra.Command, namedFlagSets flagsets.NamedFlagSets) {
  194. usageFmt := "Usage:\n %s\n"
  195. cols, _, _ := TerminalSize(cmd.OutOrStdout())
  196. cmd.SetUsageFunc(func(cmd *cobra.Command) error {
  197. fmt.Fprintf(cmd.OutOrStderr(), usageFmt, cmd.UseLine())
  198. flagsets.PrintSections(cmd.OutOrStderr(), namedFlagSets, cols)
  199. return nil
  200. })
  201. cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
  202. fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n"+usageFmt, cmd.Long, cmd.UseLine())
  203. flagsets.PrintSections(cmd.OutOrStdout(), namedFlagSets, cols)
  204. })
  205. }
  206. // TerminalSize 获取当前终端宽高,非终端返回err,同时宽高返回0值
  207. func TerminalSize(w io.Writer) (int, int, error) {
  208. outFd, isTerminal := term.GetFdInfo(w)
  209. if !isTerminal {
  210. return 0, 0, fmt.Errorf("given writer is no terminal")
  211. }
  212. winSize, err := term.GetWinsize(outFd)
  213. if err != nil {
  214. return 0, 0, err
  215. }
  216. return int(winSize.Width), int(winSize.Height), nil
  217. }