fe5e611709
Previously if a cobra user didn't specify an explicit .Date header, the current time would be included in all of the generated man pages each time they were built. This causes an issue for reproducible builds, since each re-build of a package that includes the man pages will have different times listed in the man pages. To fix this, add support for SOURCE_DATE_EPOCH (which is a standardised packaging environment variable, designed to be used specifically for this purpose[1]). [1]: https://reproducible-builds.org/specs/source-date-epoch/ Signed-off-by: Aleksa Sarai <asarai@suse.de>
248 lines
7.0 KiB
Go
248 lines
7.0 KiB
Go
// Copyright 2015 Red Hat Inc. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package doc
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/cpuguy83/go-md2man/md2man"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
)
|
|
|
|
// GenManTree will generate a man page for this command and all descendants
|
|
// in the directory given. The header may be nil. This function may not work
|
|
// correctly if your command names have `-` in them. If you have `cmd` with two
|
|
// subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third`
|
|
// it is undefined which help output will be in the file `cmd-sub-third.1`.
|
|
func GenManTree(cmd *cobra.Command, header *GenManHeader, dir string) error {
|
|
return GenManTreeFromOpts(cmd, GenManTreeOptions{
|
|
Header: header,
|
|
Path: dir,
|
|
CommandSeparator: "-",
|
|
})
|
|
}
|
|
|
|
// GenManTreeFromOpts generates a man page for the command and all descendants.
|
|
// The pages are written to the opts.Path directory.
|
|
func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error {
|
|
header := opts.Header
|
|
if header == nil {
|
|
header = &GenManHeader{}
|
|
}
|
|
for _, c := range cmd.Commands() {
|
|
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
|
continue
|
|
}
|
|
if err := GenManTreeFromOpts(c, opts); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
section := "1"
|
|
if header.Section != "" {
|
|
section = header.Section
|
|
}
|
|
|
|
separator := "_"
|
|
if opts.CommandSeparator != "" {
|
|
separator = opts.CommandSeparator
|
|
}
|
|
basename := strings.Replace(cmd.CommandPath(), " ", separator, -1)
|
|
filename := filepath.Join(opts.Path, basename+"."+section)
|
|
f, err := os.Create(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
headerCopy := *header
|
|
return GenMan(cmd, &headerCopy, f)
|
|
}
|
|
|
|
// GenManTreeOptions is the options for generating the man pages.
|
|
// Used only in GenManTreeFromOpts.
|
|
type GenManTreeOptions struct {
|
|
Header *GenManHeader
|
|
Path string
|
|
CommandSeparator string
|
|
}
|
|
|
|
// GenManHeader is a lot like the .TH header at the start of man pages. These
|
|
// include the title, section, date, source, and manual. We will use the
|
|
// current time if Date is unset and will use "Auto generated by spf13/cobra"
|
|
// if the Source is unset.
|
|
type GenManHeader struct {
|
|
Title string
|
|
Section string
|
|
Date *time.Time
|
|
date string
|
|
Source string
|
|
Manual string
|
|
}
|
|
|
|
// GenMan will generate a man page for the given command and write it to
|
|
// w. The header argument may be nil, however obviously w may not.
|
|
func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error {
|
|
if header == nil {
|
|
header = &GenManHeader{}
|
|
}
|
|
if err := fillHeader(header, cmd.CommandPath()); err != nil {
|
|
return err
|
|
}
|
|
|
|
b := genMan(cmd, header)
|
|
_, err := w.Write(md2man.Render(b))
|
|
return err
|
|
}
|
|
|
|
func fillHeader(header *GenManHeader, name string) error {
|
|
if header.Title == "" {
|
|
header.Title = strings.ToUpper(strings.Replace(name, " ", "\\-", -1))
|
|
}
|
|
if header.Section == "" {
|
|
header.Section = "1"
|
|
}
|
|
if header.Date == nil {
|
|
now := time.Now()
|
|
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
|
|
unixEpoch, err := strconv.ParseInt(epoch, 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err)
|
|
}
|
|
now = time.Unix(unixEpoch, 0)
|
|
}
|
|
header.Date = &now
|
|
}
|
|
header.date = (*header.Date).Format("Jan 2006")
|
|
if header.Source == "" {
|
|
header.Source = "Auto generated by spf13/cobra"
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func manPreamble(buf *bytes.Buffer, header *GenManHeader, cmd *cobra.Command, dashedName string) {
|
|
description := cmd.Long
|
|
if len(description) == 0 {
|
|
description = cmd.Short
|
|
}
|
|
|
|
buf.WriteString(fmt.Sprintf(`%% %s(%s)%s
|
|
%% %s
|
|
%% %s
|
|
# NAME
|
|
`, header.Title, header.Section, header.date, header.Source, header.Manual))
|
|
buf.WriteString(fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short))
|
|
buf.WriteString("# SYNOPSIS\n")
|
|
buf.WriteString(fmt.Sprintf("**%s**\n\n", cmd.UseLine()))
|
|
buf.WriteString("# DESCRIPTION\n")
|
|
buf.WriteString(description + "\n\n")
|
|
}
|
|
|
|
func manPrintFlags(buf *bytes.Buffer, flags *pflag.FlagSet) {
|
|
flags.VisitAll(func(flag *pflag.Flag) {
|
|
if len(flag.Deprecated) > 0 || flag.Hidden {
|
|
return
|
|
}
|
|
format := ""
|
|
if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 {
|
|
format = fmt.Sprintf("**-%s**, **--%s**", flag.Shorthand, flag.Name)
|
|
} else {
|
|
format = fmt.Sprintf("**--%s**", flag.Name)
|
|
}
|
|
if len(flag.NoOptDefVal) > 0 {
|
|
format += "["
|
|
}
|
|
if flag.Value.Type() == "string" {
|
|
// put quotes on the value
|
|
format += "=%q"
|
|
} else {
|
|
format += "=%s"
|
|
}
|
|
if len(flag.NoOptDefVal) > 0 {
|
|
format += "]"
|
|
}
|
|
format += "\n\t%s\n\n"
|
|
buf.WriteString(fmt.Sprintf(format, flag.DefValue, flag.Usage))
|
|
})
|
|
}
|
|
|
|
func manPrintOptions(buf *bytes.Buffer, command *cobra.Command) {
|
|
flags := command.NonInheritedFlags()
|
|
if flags.HasAvailableFlags() {
|
|
buf.WriteString("# OPTIONS\n")
|
|
manPrintFlags(buf, flags)
|
|
buf.WriteString("\n")
|
|
}
|
|
flags = command.InheritedFlags()
|
|
if flags.HasAvailableFlags() {
|
|
buf.WriteString("# OPTIONS INHERITED FROM PARENT COMMANDS\n")
|
|
manPrintFlags(buf, flags)
|
|
buf.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
|
|
cmd.InitDefaultHelpCmd()
|
|
cmd.InitDefaultHelpFlag()
|
|
|
|
// something like `rootcmd-subcmd1-subcmd2`
|
|
dashCommandName := strings.Replace(cmd.CommandPath(), " ", "-", -1)
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
manPreamble(buf, header, cmd, dashCommandName)
|
|
manPrintOptions(buf, cmd)
|
|
if len(cmd.Example) > 0 {
|
|
buf.WriteString("# EXAMPLE\n")
|
|
buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example))
|
|
}
|
|
if hasSeeAlso(cmd) {
|
|
buf.WriteString("# SEE ALSO\n")
|
|
seealsos := make([]string, 0)
|
|
if cmd.HasParent() {
|
|
parentPath := cmd.Parent().CommandPath()
|
|
dashParentPath := strings.Replace(parentPath, " ", "-", -1)
|
|
seealso := fmt.Sprintf("**%s(%s)**", dashParentPath, header.Section)
|
|
seealsos = append(seealsos, seealso)
|
|
cmd.VisitParents(func(c *cobra.Command) {
|
|
if c.DisableAutoGenTag {
|
|
cmd.DisableAutoGenTag = c.DisableAutoGenTag
|
|
}
|
|
})
|
|
}
|
|
children := cmd.Commands()
|
|
sort.Sort(byName(children))
|
|
for _, c := range children {
|
|
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
|
continue
|
|
}
|
|
seealso := fmt.Sprintf("**%s-%s(%s)**", dashCommandName, c.Name(), header.Section)
|
|
seealsos = append(seealsos, seealso)
|
|
}
|
|
buf.WriteString(strings.Join(seealsos, ", ") + "\n")
|
|
}
|
|
if !cmd.DisableAutoGenTag {
|
|
buf.WriteString(fmt.Sprintf("# HISTORY\n%s Auto generated by spf13/cobra\n", header.Date.Format("2-Jan-2006")))
|
|
}
|
|
return buf.Bytes()
|
|
}
|