man_docs.go

  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}