cmdflag.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. // Package cmdflag adds single level cmdflag support to the standard library flag package.
  2. package cmdflag
  3. import (
  4. "flag"
  5. "fmt"
  6. "io"
  7. "os"
  8. "path/filepath"
  9. "runtime"
  10. "runtime/debug"
  11. "strings"
  12. "sync"
  13. )
  14. //TODO add a -fullversion command to display module versions, compiler version
  15. //TODO add multi level command support
  16. // VersionBoolFlag is the flag name to be used as a boolean flag to display the program version.
  17. const VersionBoolFlag = "version"
  18. // Usage is the function used for help.
  19. var Usage = func() {
  20. fset := flag.CommandLine
  21. out := fset.Output()
  22. program := programName(os.Args[0])
  23. fmt.Fprintf(out, "Usage of %s:\n", program)
  24. fset.PrintDefaults()
  25. fmt.Fprintf(out, "\nSubcommands:")
  26. for _, sub := range subs {
  27. fmt.Fprintf(out, "\n%s\n%s %s\n", sub.desc, sub.name, sub.argsdesc)
  28. fs, _ := sub.init(out)
  29. fs.PrintDefaults()
  30. }
  31. }
  32. // Handler is the function called when a matching cmdflag is found.
  33. type Handler func(...string) error
  34. type subcmd struct {
  35. errh flag.ErrorHandling
  36. name string
  37. argsdesc string
  38. desc string
  39. ini func(*flag.FlagSet) Handler
  40. }
  41. func (sub *subcmd) init(out io.Writer) (*flag.FlagSet, Handler) {
  42. fname := fmt.Sprintf("cmdflag `%s`", sub.name)
  43. fs := flag.NewFlagSet(fname, sub.errh)
  44. fs.SetOutput(out)
  45. return fs, sub.ini(fs)
  46. }
  47. var (
  48. mu sync.Mutex
  49. subs []*subcmd
  50. )
  51. // New instantiates a new cmdflag with its name and description.
  52. //
  53. // It is safe to be called from multiple go routines (typically in init functions).
  54. //
  55. // The cmdflag initializer is called only when the cmdflag is present on the command line.
  56. // The handler is called with the remaining arguments once the cmdflag flags have been parsed successfully.
  57. //
  58. // It will panic if the cmdflag already exists.
  59. func New(name, argsdesc, desc string, errh flag.ErrorHandling, initializer func(*flag.FlagSet) Handler) {
  60. sub := &subcmd{
  61. errh: errh,
  62. name: name,
  63. argsdesc: argsdesc,
  64. desc: desc,
  65. ini: initializer,
  66. }
  67. mu.Lock()
  68. defer mu.Unlock()
  69. for _, sub := range subs {
  70. if sub.name == name {
  71. panic(fmt.Errorf("cmdflag %s redeclared", name))
  72. }
  73. }
  74. subs = append(subs, sub)
  75. }
  76. // Parse parses the command line arguments including the global flags and, if any, the cmdflag and its flags.
  77. //
  78. // If the VersionBoolFlag is defined as a global boolean flag, then the program version is displayed and the program stops.
  79. func Parse() error {
  80. args := os.Args
  81. if len(args) == 1 {
  82. return nil
  83. }
  84. // Global flags.
  85. fset := flag.CommandLine
  86. fset.Usage = Usage
  87. out := fset.Output()
  88. if err := fset.Parse(args[1:]); err != nil {
  89. return err
  90. }
  91. // Handle version request.
  92. if f := fset.Lookup(VersionBoolFlag); f != nil {
  93. if v, ok := f.Value.(flag.Getter); ok {
  94. // All values implemented by the flag package implement the flag.Getter interface.
  95. if b, ok := v.Get().(bool); ok && b {
  96. // The flag was defined as a bool and is set.
  97. program := programName(args[0])
  98. if bi, ok := debug.ReadBuildInfo(); ok {
  99. fmt.Fprintf(out, "%s version %s", program, bi.Main.Version)
  100. } else {
  101. fmt.Fprintf(out, "%s no version available (not built with module support)", program)
  102. }
  103. fmt.Fprintf(out, " %s/%s\n", runtime.GOOS, runtime.GOARCH)
  104. return nil
  105. }
  106. }
  107. }
  108. // No cmdflag.
  109. if fset.NArg() == 0 {
  110. return nil
  111. }
  112. // Subcommand.
  113. idx := len(args) - fset.NArg()
  114. s := args[idx]
  115. args = args[idx+1:]
  116. for _, sub := range subs {
  117. if sub.name != s {
  118. continue
  119. }
  120. fs, handler := sub.init(out)
  121. if err := fs.Parse(args); err != nil {
  122. return err
  123. }
  124. return handler(args[len(args)-fs.NArg():]...)
  125. }
  126. return fmt.Errorf("%s is not a valid cmdflag", s)
  127. }
  128. func programName(s string) string {
  129. name := filepath.Base(s)
  130. return strings.TrimSuffix(name, ".exe")
  131. }