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.

help.go 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. // Copyright 2012 Jesse van den Kieboom. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. package flags
  5. import (
  6. "bufio"
  7. "bytes"
  8. "fmt"
  9. "io"
  10. "runtime"
  11. "strings"
  12. "unicode/utf8"
  13. )
  14. type alignmentInfo struct {
  15. maxLongLen int
  16. hasShort bool
  17. hasValueName bool
  18. terminalColumns int
  19. indent bool
  20. }
  21. const (
  22. paddingBeforeOption = 2
  23. distanceBetweenOptionAndDescription = 2
  24. )
  25. func (a *alignmentInfo) descriptionStart() int {
  26. ret := a.maxLongLen + distanceBetweenOptionAndDescription
  27. if a.hasShort {
  28. ret += 2
  29. }
  30. if a.maxLongLen > 0 {
  31. ret += 4
  32. }
  33. if a.hasValueName {
  34. ret += 3
  35. }
  36. return ret
  37. }
  38. func (a *alignmentInfo) updateLen(name string, indent bool) {
  39. l := utf8.RuneCountInString(name)
  40. if indent {
  41. l = l + 4
  42. }
  43. if l > a.maxLongLen {
  44. a.maxLongLen = l
  45. }
  46. }
  47. func (p *Parser) getAlignmentInfo() alignmentInfo {
  48. ret := alignmentInfo{
  49. maxLongLen: 0,
  50. hasShort: false,
  51. hasValueName: false,
  52. terminalColumns: getTerminalColumns(),
  53. }
  54. if ret.terminalColumns <= 0 {
  55. ret.terminalColumns = 80
  56. }
  57. var prevcmd *Command
  58. p.eachActiveGroup(func(c *Command, grp *Group) {
  59. if c != prevcmd {
  60. for _, arg := range c.args {
  61. ret.updateLen(arg.Name, c != p.Command)
  62. }
  63. }
  64. for _, info := range grp.options {
  65. if !info.canCli() {
  66. continue
  67. }
  68. if info.ShortName != 0 {
  69. ret.hasShort = true
  70. }
  71. if len(info.ValueName) > 0 {
  72. ret.hasValueName = true
  73. }
  74. l := info.LongNameWithNamespace() + info.ValueName
  75. if len(info.Choices) != 0 {
  76. l += "[" + strings.Join(info.Choices, "|") + "]"
  77. }
  78. ret.updateLen(l, c != p.Command)
  79. }
  80. })
  81. return ret
  82. }
  83. func wrapText(s string, l int, prefix string) string {
  84. var ret string
  85. if l < 10 {
  86. l = 10
  87. }
  88. // Basic text wrapping of s at spaces to fit in l
  89. lines := strings.Split(s, "\n")
  90. for _, line := range lines {
  91. var retline string
  92. line = strings.TrimSpace(line)
  93. for len(line) > l {
  94. // Try to split on space
  95. suffix := ""
  96. pos := strings.LastIndex(line[:l], " ")
  97. if pos < 0 {
  98. pos = l - 1
  99. suffix = "-\n"
  100. }
  101. if len(retline) != 0 {
  102. retline += "\n" + prefix
  103. }
  104. retline += strings.TrimSpace(line[:pos]) + suffix
  105. line = strings.TrimSpace(line[pos:])
  106. }
  107. if len(line) > 0 {
  108. if len(retline) != 0 {
  109. retline += "\n" + prefix
  110. }
  111. retline += line
  112. }
  113. if len(ret) > 0 {
  114. ret += "\n"
  115. if len(retline) > 0 {
  116. ret += prefix
  117. }
  118. }
  119. ret += retline
  120. }
  121. return ret
  122. }
  123. func (p *Parser) writeHelpOption(writer *bufio.Writer, option *Option, info alignmentInfo) {
  124. line := &bytes.Buffer{}
  125. prefix := paddingBeforeOption
  126. if info.indent {
  127. prefix += 4
  128. }
  129. if option.Hidden {
  130. return
  131. }
  132. line.WriteString(strings.Repeat(" ", prefix))
  133. if option.ShortName != 0 {
  134. line.WriteRune(defaultShortOptDelimiter)
  135. line.WriteRune(option.ShortName)
  136. } else if info.hasShort {
  137. line.WriteString(" ")
  138. }
  139. descstart := info.descriptionStart() + paddingBeforeOption
  140. if len(option.LongName) > 0 {
  141. if option.ShortName != 0 {
  142. line.WriteString(", ")
  143. } else if info.hasShort {
  144. line.WriteString(" ")
  145. }
  146. line.WriteString(defaultLongOptDelimiter)
  147. line.WriteString(option.LongNameWithNamespace())
  148. }
  149. if option.canArgument() {
  150. line.WriteRune(defaultNameArgDelimiter)
  151. if len(option.ValueName) > 0 {
  152. line.WriteString(option.ValueName)
  153. }
  154. if len(option.Choices) > 0 {
  155. line.WriteString("[" + strings.Join(option.Choices, "|") + "]")
  156. }
  157. }
  158. written := line.Len()
  159. line.WriteTo(writer)
  160. if option.Description != "" {
  161. dw := descstart - written
  162. writer.WriteString(strings.Repeat(" ", dw))
  163. var def string
  164. if len(option.DefaultMask) != 0 {
  165. if option.DefaultMask != "-" {
  166. def = option.DefaultMask
  167. }
  168. } else {
  169. def = option.defaultLiteral
  170. }
  171. var envDef string
  172. if option.EnvDefaultKey != "" {
  173. var envPrintable string
  174. if runtime.GOOS == "windows" {
  175. envPrintable = "%" + option.EnvDefaultKey + "%"
  176. } else {
  177. envPrintable = "$" + option.EnvDefaultKey
  178. }
  179. envDef = fmt.Sprintf(" [%s]", envPrintable)
  180. }
  181. var desc string
  182. if def != "" {
  183. desc = fmt.Sprintf("%s (default: %v)%s", option.Description, def, envDef)
  184. } else {
  185. desc = option.Description + envDef
  186. }
  187. writer.WriteString(wrapText(desc,
  188. info.terminalColumns-descstart,
  189. strings.Repeat(" ", descstart)))
  190. }
  191. writer.WriteString("\n")
  192. }
  193. func maxCommandLength(s []*Command) int {
  194. if len(s) == 0 {
  195. return 0
  196. }
  197. ret := len(s[0].Name)
  198. for _, v := range s[1:] {
  199. l := len(v.Name)
  200. if l > ret {
  201. ret = l
  202. }
  203. }
  204. return ret
  205. }
  206. // WriteHelp writes a help message containing all the possible options and
  207. // their descriptions to the provided writer. Note that the HelpFlag parser
  208. // option provides a convenient way to add a -h/--help option group to the
  209. // command line parser which will automatically show the help messages using
  210. // this method.
  211. func (p *Parser) WriteHelp(writer io.Writer) {
  212. if writer == nil {
  213. return
  214. }
  215. wr := bufio.NewWriter(writer)
  216. aligninfo := p.getAlignmentInfo()
  217. cmd := p.Command
  218. for cmd.Active != nil {
  219. cmd = cmd.Active
  220. }
  221. if p.Name != "" {
  222. wr.WriteString("Usage:\n")
  223. wr.WriteString(" ")
  224. allcmd := p.Command
  225. for allcmd != nil {
  226. var usage string
  227. if allcmd == p.Command {
  228. if len(p.Usage) != 0 {
  229. usage = p.Usage
  230. } else if p.Options&HelpFlag != 0 {
  231. usage = "[OPTIONS]"
  232. }
  233. } else if us, ok := allcmd.data.(Usage); ok {
  234. usage = us.Usage()
  235. } else if allcmd.hasCliOptions() {
  236. usage = fmt.Sprintf("[%s-OPTIONS]", allcmd.Name)
  237. }
  238. if len(usage) != 0 {
  239. fmt.Fprintf(wr, " %s %s", allcmd.Name, usage)
  240. } else {
  241. fmt.Fprintf(wr, " %s", allcmd.Name)
  242. }
  243. if len(allcmd.args) > 0 {
  244. fmt.Fprintf(wr, " ")
  245. }
  246. for i, arg := range allcmd.args {
  247. if i != 0 {
  248. fmt.Fprintf(wr, " ")
  249. }
  250. name := arg.Name
  251. if arg.isRemaining() {
  252. name = name + "..."
  253. }
  254. if !allcmd.ArgsRequired {
  255. fmt.Fprintf(wr, "[%s]", name)
  256. } else {
  257. fmt.Fprintf(wr, "%s", name)
  258. }
  259. }
  260. if allcmd.Active == nil && len(allcmd.commands) > 0 {
  261. var co, cc string
  262. if allcmd.SubcommandsOptional {
  263. co, cc = "[", "]"
  264. } else {
  265. co, cc = "<", ">"
  266. }
  267. visibleCommands := allcmd.visibleCommands()
  268. if len(visibleCommands) > 3 {
  269. fmt.Fprintf(wr, " %scommand%s", co, cc)
  270. } else {
  271. subcommands := allcmd.sortedVisibleCommands()
  272. names := make([]string, len(subcommands))
  273. for i, subc := range subcommands {
  274. names[i] = subc.Name
  275. }
  276. fmt.Fprintf(wr, " %s%s%s", co, strings.Join(names, " | "), cc)
  277. }
  278. }
  279. allcmd = allcmd.Active
  280. }
  281. fmt.Fprintln(wr)
  282. if len(cmd.LongDescription) != 0 {
  283. fmt.Fprintln(wr)
  284. t := wrapText(cmd.LongDescription,
  285. aligninfo.terminalColumns,
  286. "")
  287. fmt.Fprintln(wr, t)
  288. }
  289. }
  290. c := p.Command
  291. for c != nil {
  292. printcmd := c != p.Command
  293. c.eachGroup(func(grp *Group) {
  294. first := true
  295. // Skip built-in help group for all commands except the top-level
  296. // parser
  297. if grp.Hidden || (grp.isBuiltinHelp && c != p.Command) {
  298. return
  299. }
  300. for _, info := range grp.options {
  301. if !info.canCli() || info.Hidden {
  302. continue
  303. }
  304. if printcmd {
  305. fmt.Fprintf(wr, "\n[%s command options]\n", c.Name)
  306. aligninfo.indent = true
  307. printcmd = false
  308. }
  309. if first && cmd.Group != grp {
  310. fmt.Fprintln(wr)
  311. if aligninfo.indent {
  312. wr.WriteString(" ")
  313. }
  314. fmt.Fprintf(wr, "%s:\n", grp.ShortDescription)
  315. first = false
  316. }
  317. p.writeHelpOption(wr, info, aligninfo)
  318. }
  319. })
  320. var args []*Arg
  321. for _, arg := range c.args {
  322. if arg.Description != "" {
  323. args = append(args, arg)
  324. }
  325. }
  326. if len(args) > 0 {
  327. if c == p.Command {
  328. fmt.Fprintf(wr, "\nArguments:\n")
  329. } else {
  330. fmt.Fprintf(wr, "\n[%s command arguments]\n", c.Name)
  331. }
  332. descStart := aligninfo.descriptionStart() + paddingBeforeOption
  333. for _, arg := range args {
  334. argPrefix := strings.Repeat(" ", paddingBeforeOption)
  335. argPrefix += arg.Name
  336. if len(arg.Description) > 0 {
  337. argPrefix += ":"
  338. wr.WriteString(argPrefix)
  339. // Space between "arg:" and the description start
  340. descPadding := strings.Repeat(" ", descStart-len(argPrefix))
  341. // How much space the description gets before wrapping
  342. descWidth := aligninfo.terminalColumns - 1 - descStart
  343. // Whitespace to which we can indent new description lines
  344. descPrefix := strings.Repeat(" ", descStart)
  345. wr.WriteString(descPadding)
  346. wr.WriteString(wrapText(arg.Description, descWidth, descPrefix))
  347. } else {
  348. wr.WriteString(argPrefix)
  349. }
  350. fmt.Fprintln(wr)
  351. }
  352. }
  353. c = c.Active
  354. }
  355. scommands := cmd.sortedVisibleCommands()
  356. if len(scommands) > 0 {
  357. maxnamelen := maxCommandLength(scommands)
  358. fmt.Fprintln(wr)
  359. fmt.Fprintln(wr, "Available commands:")
  360. for _, c := range scommands {
  361. fmt.Fprintf(wr, " %s", c.Name)
  362. if len(c.ShortDescription) > 0 {
  363. pad := strings.Repeat(" ", maxnamelen-len(c.Name))
  364. fmt.Fprintf(wr, "%s %s", pad, c.ShortDescription)
  365. if len(c.Aliases) > 0 {
  366. fmt.Fprintf(wr, " (aliases: %s)", strings.Join(c.Aliases, ", "))
  367. }
  368. }
  369. fmt.Fprintln(wr)
  370. }
  371. }
  372. wr.Flush()
  373. }