cmdflag.go 3.5 KB

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