yaml.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package yaml
  14. import (
  15. "fmt"
  16. "io"
  17. "os"
  18. "path/filepath"
  19. "sort"
  20. "strings"
  21. "github.com/spf13/cobra"
  22. "github.com/spf13/pflag"
  23. yaml "gopkg.in/yaml.v3"
  24. )
  25. type cmdOption struct {
  26. Option string
  27. Shorthand string `yaml:",omitempty"`
  28. ValueType string `yaml:"value_type,omitempty"`
  29. DefaultValue string `yaml:"default_value,omitempty"`
  30. Description string `yaml:",omitempty"`
  31. Deprecated bool
  32. MinAPIVersion string `yaml:"min_api_version,omitempty"`
  33. Experimental bool
  34. ExperimentalCLI bool
  35. Kubernetes bool
  36. Swarm bool
  37. OSType string `yaml:"os_type,omitempty"`
  38. }
  39. type cmdDoc struct {
  40. Name string `yaml:"command"`
  41. SeeAlso []string `yaml:"parent,omitempty"`
  42. Version string `yaml:"engine_version,omitempty"`
  43. Aliases string `yaml:",omitempty"`
  44. Short string `yaml:",omitempty"`
  45. Long string `yaml:",omitempty"`
  46. Usage string `yaml:",omitempty"`
  47. Pname string `yaml:",omitempty"`
  48. Plink string `yaml:",omitempty"`
  49. Cname []string `yaml:",omitempty"`
  50. Clink []string `yaml:",omitempty"`
  51. Options []cmdOption `yaml:",omitempty"`
  52. InheritedOptions []cmdOption `yaml:"inherited_options,omitempty"`
  53. Example string `yaml:"examples,omitempty"`
  54. Deprecated bool
  55. MinAPIVersion string `yaml:"min_api_version,omitempty"`
  56. Experimental bool
  57. ExperimentalCLI bool
  58. Kubernetes bool
  59. Swarm bool
  60. OSType string `yaml:"os_type,omitempty"`
  61. }
  62. // GenYamlTree creates yaml structured ref files
  63. func GenYamlTree(cmd *cobra.Command, dir string) error {
  64. emptyStr := func(s string) string { return "" }
  65. return GenYamlTreeCustom(cmd, dir, emptyStr)
  66. }
  67. // GenYamlTreeCustom creates yaml structured ref files
  68. func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string) string) error {
  69. for _, c := range cmd.Commands() {
  70. if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
  71. continue
  72. }
  73. if err := GenYamlTreeCustom(c, dir, filePrepender); err != nil {
  74. return err
  75. }
  76. }
  77. if !cmd.HasParent() {
  78. return nil
  79. }
  80. basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".yaml"
  81. filename := filepath.Join(dir, basename)
  82. f, err := os.Create(filename)
  83. if err != nil {
  84. return err
  85. }
  86. defer f.Close() //nolint: errcheck
  87. if _, err := io.WriteString(f, filePrepender(filename)); err != nil {
  88. return err
  89. }
  90. return GenYamlCustom(cmd, f)
  91. }
  92. // GenYamlCustom creates custom yaml output
  93. // nolint: gocyclo
  94. func GenYamlCustom(cmd *cobra.Command, w io.Writer) error {
  95. cliDoc := cmdDoc{}
  96. cliDoc.Name = cmd.CommandPath()
  97. cliDoc.Aliases = strings.Join(cmd.Aliases, ", ")
  98. cliDoc.Short = cmd.Short
  99. cliDoc.Long = cmd.Long
  100. if len(cliDoc.Long) == 0 {
  101. cliDoc.Long = cliDoc.Short
  102. }
  103. if cmd.Runnable() {
  104. cliDoc.Usage = cmd.UseLine()
  105. }
  106. if len(cmd.Example) > 0 {
  107. cliDoc.Example = cmd.Example
  108. }
  109. if len(cmd.Deprecated) > 0 {
  110. cliDoc.Deprecated = true
  111. }
  112. // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
  113. for curr := cmd; curr != nil; curr = curr.Parent() {
  114. if v, ok := curr.Annotations["version"]; ok && cliDoc.MinAPIVersion == "" {
  115. cliDoc.MinAPIVersion = v
  116. }
  117. if _, ok := curr.Annotations["experimental"]; ok && !cliDoc.Experimental {
  118. cliDoc.Experimental = true
  119. }
  120. if _, ok := curr.Annotations["experimentalCLI"]; ok && !cliDoc.ExperimentalCLI {
  121. cliDoc.ExperimentalCLI = true
  122. }
  123. if _, ok := curr.Annotations["kubernetes"]; ok && !cliDoc.Kubernetes {
  124. cliDoc.Kubernetes = true
  125. }
  126. if _, ok := curr.Annotations["swarm"]; ok && !cliDoc.Swarm {
  127. cliDoc.Swarm = true
  128. }
  129. if os, ok := curr.Annotations["ostype"]; ok && cliDoc.OSType == "" {
  130. cliDoc.OSType = os
  131. }
  132. }
  133. flags := cmd.NonInheritedFlags()
  134. if flags.HasFlags() {
  135. cliDoc.Options = genFlagResult(flags)
  136. }
  137. flags = cmd.InheritedFlags()
  138. if flags.HasFlags() {
  139. cliDoc.InheritedOptions = genFlagResult(flags)
  140. }
  141. if hasSeeAlso(cmd) {
  142. if cmd.HasParent() {
  143. parent := cmd.Parent()
  144. cliDoc.Pname = parent.CommandPath()
  145. link := cliDoc.Pname + ".yaml"
  146. cliDoc.Plink = strings.Replace(link, " ", "_", -1)
  147. cmd.VisitParents(func(c *cobra.Command) {
  148. if c.DisableAutoGenTag {
  149. cmd.DisableAutoGenTag = c.DisableAutoGenTag
  150. }
  151. })
  152. }
  153. children := cmd.Commands()
  154. sort.Sort(byName(children))
  155. for _, child := range children {
  156. if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() {
  157. continue
  158. }
  159. currentChild := cliDoc.Name + " " + child.Name()
  160. cliDoc.Cname = append(cliDoc.Cname, cliDoc.Name+" "+child.Name())
  161. link := currentChild + ".yaml"
  162. cliDoc.Clink = append(cliDoc.Clink, strings.Replace(link, " ", "_", -1))
  163. }
  164. }
  165. final, err := yaml.Marshal(&cliDoc)
  166. if err != nil {
  167. fmt.Println(err)
  168. os.Exit(1)
  169. }
  170. if _, err := fmt.Fprintln(w, string(final)); err != nil {
  171. return err
  172. }
  173. return nil
  174. }
  175. func genFlagResult(flags *pflag.FlagSet) []cmdOption {
  176. var (
  177. result []cmdOption
  178. opt cmdOption
  179. )
  180. flags.VisitAll(func(flag *pflag.Flag) {
  181. opt = cmdOption{
  182. Option: flag.Name,
  183. ValueType: flag.Value.Type(),
  184. DefaultValue: forceMultiLine(flag.DefValue),
  185. Description: forceMultiLine(flag.Usage),
  186. Deprecated: len(flag.Deprecated) > 0,
  187. }
  188. // Todo, when we mark a shorthand is deprecated, but specify an empty message.
  189. // The flag.ShorthandDeprecated is empty as the shorthand is deprecated.
  190. // Using len(flag.ShorthandDeprecated) > 0 can't handle this, others are ok.
  191. if !(len(flag.ShorthandDeprecated) > 0) && len(flag.Shorthand) > 0 {
  192. opt.Shorthand = flag.Shorthand
  193. }
  194. if _, ok := flag.Annotations["experimental"]; ok {
  195. opt.Experimental = true
  196. }
  197. if v, ok := flag.Annotations["version"]; ok {
  198. opt.MinAPIVersion = v[0]
  199. }
  200. if _, ok := flag.Annotations["experimentalCLI"]; ok {
  201. opt.ExperimentalCLI = true
  202. }
  203. if _, ok := flag.Annotations["kubernetes"]; ok {
  204. opt.Kubernetes = true
  205. }
  206. if _, ok := flag.Annotations["swarm"]; ok {
  207. opt.Swarm = true
  208. }
  209. // Note that the annotation can have multiple ostypes set, however, multiple
  210. // values are currently not used (and unlikely will).
  211. //
  212. // To simplify usage of the os_type property in the YAML, and for consistency
  213. // with the same property for commands, we're only using the first ostype that's set.
  214. if ostypes, ok := flag.Annotations["ostype"]; ok && len(opt.OSType) == 0 && len(ostypes) > 0 {
  215. opt.OSType = ostypes[0]
  216. }
  217. result = append(result, opt)
  218. })
  219. return result
  220. }
  221. // Temporary workaround for yaml lib generating incorrect yaml with long strings
  222. // that do not contain \n.
  223. func forceMultiLine(s string) string {
  224. if len(s) > 60 && !strings.Contains(s, "\n") {
  225. s = s + "\n"
  226. }
  227. return s
  228. }
  229. // Small duplication for cobra utils
  230. func hasSeeAlso(cmd *cobra.Command) bool {
  231. if cmd.HasParent() {
  232. return true
  233. }
  234. for _, c := range cmd.Commands() {
  235. if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
  236. continue
  237. }
  238. return true
  239. }
  240. return false
  241. }
  242. // ParseMDContent parse markdown file looking for Description and Examples sections
  243. func ParseMDContent(mdString string) (description string, examples string) {
  244. parsedContent := strings.Split(mdString, "\n## ")
  245. for _, s := range parsedContent {
  246. if strings.Index(s, "Description") == 0 {
  247. description = strings.TrimSpace(strings.TrimPrefix(s, "Description"))
  248. }
  249. if strings.Index(s, "Examples") == 0 {
  250. examples = strings.TrimSpace(strings.TrimPrefix(s, "Examples"))
  251. }
  252. }
  253. return description, examples
  254. }
  255. type byName []*cobra.Command
  256. func (s byName) Len() int { return len(s) }
  257. func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
  258. func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }