1// Copyright 2015 Red Hat Inc. All rights reserved.
2//
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//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14package doc
15
16import (
17 "bytes"
18 "fmt"
19 "io"
20 "os"
21 "path/filepath"
22 "sort"
23 "strconv"
24 "strings"
25 "time"
26
27 "github.com/cpuguy83/go-md2man/md2man"
28 "github.com/spf13/cobra"
29 "github.com/spf13/pflag"
30)
31
32// GenManTree will generate a man page for this command and all descendants
33// in the directory given. The header may be nil. This function may not work
34// correctly if your command names have `-` in them. If you have `cmd` with two
35// subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third`
36// it is undefined which help output will be in the file `cmd-sub-third.1`.
37func GenManTree(cmd *cobra.Command, header *GenManHeader, dir string) error {
38 return GenManTreeFromOpts(cmd, GenManTreeOptions{
39 Header: header,
40 Path: dir,
41 CommandSeparator: "-",
42 })
43}
44
45// GenManTreeFromOpts generates a man page for the command and all descendants.
46// The pages are written to the opts.Path directory.
47func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error {
48 header := opts.Header
49 if header == nil {
50 header = &GenManHeader{}
51 }
52 for _, c := range cmd.Commands() {
53 if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
54 continue
55 }
56 if err := GenManTreeFromOpts(c, opts); err != nil {
57 return err
58 }
59 }
60 section := "1"
61 if header.Section != "" {
62 section = header.Section
63 }
64
65 separator := "_"
66 if opts.CommandSeparator != "" {
67 separator = opts.CommandSeparator
68 }
69 basename := strings.Replace(cmd.CommandPath(), " ", separator, -1)
70 filename := filepath.Join(opts.Path, basename+"."+section)
71 f, err := os.Create(filename)
72 if err != nil {
73 return err
74 }
75 defer f.Close()
76
77 headerCopy := *header
78 return GenMan(cmd, &headerCopy, f)
79}
80
81// GenManTreeOptions is the options for generating the man pages.
82// Used only in GenManTreeFromOpts.
83type GenManTreeOptions struct {
84 Header *GenManHeader
85 Path string
86 CommandSeparator string
87}
88
89// GenManHeader is a lot like the .TH header at the start of man pages. These
90// include the title, section, date, source, and manual. We will use the
91// current time if Date is unset and will use "Auto generated by spf13/cobra"
92// if the Source is unset.
93type GenManHeader struct {
94 Title string
95 Section string
96 Date *time.Time
97 date string
98 Source string
99 Manual string
100}
101
102// GenMan will generate a man page for the given command and write it to
103// w. The header argument may be nil, however obviously w may not.
104func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error {
105 if header == nil {
106 header = &GenManHeader{}
107 }
108 if err := fillHeader(header, cmd.CommandPath()); err != nil {
109 return err
110 }
111
112 b := genMan(cmd, header)
113 _, err := w.Write(md2man.Render(b))
114 return err
115}
116
117func fillHeader(header *GenManHeader, name string) error {
118 if header.Title == "" {
119 header.Title = strings.ToUpper(strings.Replace(name, " ", "\\-", -1))
120 }
121 if header.Section == "" {
122 header.Section = "1"
123 }
124 if header.Date == nil {
125 now := time.Now()
126 if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
127 unixEpoch, err := strconv.ParseInt(epoch, 10, 64)
128 if err != nil {
129 return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err)
130 }
131 now = time.Unix(unixEpoch, 0)
132 }
133 header.Date = &now
134 }
135 header.date = (*header.Date).Format("Jan 2006")
136 if header.Source == "" {
137 header.Source = "Auto generated by spf13/cobra"
138 }
139 return nil
140}
141
142func manPreamble(buf *bytes.Buffer, header *GenManHeader, cmd *cobra.Command, dashedName string) {
143 description := cmd.Long
144 if len(description) == 0 {
145 description = cmd.Short
146 }
147
148 buf.WriteString(fmt.Sprintf(`%% %s(%s)%s
149%% %s
150%% %s
151# NAME
152`, header.Title, header.Section, header.date, header.Source, header.Manual))
153 buf.WriteString(fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short))
154 buf.WriteString("# SYNOPSIS\n")
155 buf.WriteString(fmt.Sprintf("**%s**\n\n", cmd.UseLine()))
156 buf.WriteString("# DESCRIPTION\n")
157 buf.WriteString(description + "\n\n")
158}
159
160func manPrintFlags(buf *bytes.Buffer, flags *pflag.FlagSet) {
161 flags.VisitAll(func(flag *pflag.Flag) {
162 if len(flag.Deprecated) > 0 || flag.Hidden {
163 return
164 }
165 format := ""
166 if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 {
167 format = fmt.Sprintf("**-%s**, **--%s**", flag.Shorthand, flag.Name)
168 } else {
169 format = fmt.Sprintf("**--%s**", flag.Name)
170 }
171 if len(flag.NoOptDefVal) > 0 {
172 format += "["
173 }
174 if flag.Value.Type() == "string" {
175 // put quotes on the value
176 format += "=%q"
177 } else {
178 format += "=%s"
179 }
180 if len(flag.NoOptDefVal) > 0 {
181 format += "]"
182 }
183 format += "\n\t%s\n\n"
184 buf.WriteString(fmt.Sprintf(format, flag.DefValue, flag.Usage))
185 })
186}
187
188func manPrintOptions(buf *bytes.Buffer, command *cobra.Command) {
189 flags := command.NonInheritedFlags()
190 if flags.HasAvailableFlags() {
191 buf.WriteString("# OPTIONS\n")
192 manPrintFlags(buf, flags)
193 buf.WriteString("\n")
194 }
195 flags = command.InheritedFlags()
196 if flags.HasAvailableFlags() {
197 buf.WriteString("# OPTIONS INHERITED FROM PARENT COMMANDS\n")
198 manPrintFlags(buf, flags)
199 buf.WriteString("\n")
200 }
201}
202
203func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
204 cmd.InitDefaultHelpCmd()
205 cmd.InitDefaultHelpFlag()
206
207 // something like `rootcmd-subcmd1-subcmd2`
208 dashCommandName := strings.Replace(cmd.CommandPath(), " ", "-", -1)
209
210 buf := new(bytes.Buffer)
211
212 manPreamble(buf, header, cmd, dashCommandName)
213 manPrintOptions(buf, cmd)
214 if len(cmd.Example) > 0 {
215 buf.WriteString("# EXAMPLE\n")
216 buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example))
217 }
218 if hasSeeAlso(cmd) {
219 buf.WriteString("# SEE ALSO\n")
220 seealsos := make([]string, 0)
221 if cmd.HasParent() {
222 parentPath := cmd.Parent().CommandPath()
223 dashParentPath := strings.Replace(parentPath, " ", "-", -1)
224 seealso := fmt.Sprintf("**%s(%s)**", dashParentPath, header.Section)
225 seealsos = append(seealsos, seealso)
226 cmd.VisitParents(func(c *cobra.Command) {
227 if c.DisableAutoGenTag {
228 cmd.DisableAutoGenTag = c.DisableAutoGenTag
229 }
230 })
231 }
232 children := cmd.Commands()
233 sort.Sort(byName(children))
234 for _, c := range children {
235 if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
236 continue
237 }
238 seealso := fmt.Sprintf("**%s-%s(%s)**", dashCommandName, c.Name(), header.Section)
239 seealsos = append(seealsos, seealso)
240 }
241 buf.WriteString(strings.Join(seealsos, ", ") + "\n")
242 }
243 if !cmd.DisableAutoGenTag {
244 buf.WriteString(fmt.Sprintf("# HISTORY\n%s Auto generated by spf13/cobra\n", header.Date.Format("2-Jan-2006")))
245 }
246 return buf.Bytes()
247}