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