// Copyright 2013-2023 The Cobra Authors // // 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/v2/md2man" "github.com/spf13/cobra" "github.com/spf13/pflag" ) // GenManTree generates a man page for the provided command and all its descendants in the specified directory. // // It takes a cobra.Command pointer representing the root command, an optional GenManHeader pointer to customize the header information, and a string specifying the output directory. // // The function assumes that command names do not contain hyphens (`-`). If they do, unexpected behavior may occur. // // Note: If a command named `cmd` has subcommands `sub` and `sub-third`, and `sub` itself has a subcommand called `third`, it is undefined which help output will be written to the file `cmd-sub-third.1`. // // Returns an error if the generation process encounters any issues, such as invalid input or permission errors when writing files to the specified directory. 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. It recursively processes each command in the tree, // skipping non-available or additional help topic commands. Each man page is saved with a filename based on the command path and section. // Parameters: // - cmd: A pointer to the root cobra.Command for which man pages need to be generated. // - opts: A GenManTreeOptions struct containing options for generating the man pages, such as header details, output directory, and separators. // Returns: // - error: If any error occurs during the generation of the man pages, it is returned. 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.ReplaceAll(cmd.CommandPath(), " ", separator) 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 generates a man page for the given command and writes it to the specified writer. // // Parameters: // cmd - The cobra.Command for which to generate the man page. // header - A pointer to a GenManHeader that contains additional information for the man page. If nil, a default header will be used. // w - The io.Writer to which the generated man page will be written. // // Returns: // error - If an error occurs during the generation of the man page, it will be returned here. func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error { if header == nil { header = &GenManHeader{} } if err := fillHeader(header, cmd.CommandPath(), cmd.DisableAutoGenTag); err != nil { return err } b := genMan(cmd, header) _, err := w.Write(md2man.Render(b)) return err } // fillHeader populates the GenManHeader with default values if they are not already set. // // It sets the title to the uppercase version of `name`, replacing spaces with hyphens. // If the section is empty, it defaults to "1". // If the date is nil, it sets the date to the current time or a time specified by the SOURCE_DATE_EPOCH environment variable. // The formatted date is stored in header.date. // If source is empty and auto-generation is not disabled, it sets the source to "Auto generated by spf13/cobra". // // It returns an error if there is an issue parsing the SOURCE_DATE_EPOCH environment variable. func fillHeader(header *GenManHeader, name string, disableAutoGen bool) error { if header.Title == "" { header.Title = strings.ToUpper(strings.ReplaceAll(name, " ", "\\-")) } 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 == "" && !disableAutoGen { header.Source = "Auto generated by spf13/cobra" } return nil } // manPreamble writes the preamble for a manual page to the given buffer. // // Parameters: // buf - the io.StringWriter to write the preamble to. // header - a pointer to a GenManHeader containing metadata for the manual page. // cmd - a pointer to a cobra.Command representing the command being documented. // dashedName - the dash-separated name of the command. // // This function constructs the preamble section of a man page, including // the title, section, date, source, and manual. It also includes the command's // short description and synopsis. func manPreamble(buf io.StringWriter, header *GenManHeader, cmd *cobra.Command, dashedName string) { description := cmd.Long if len(description) == 0 { description = cmd.Short } cobra.WriteStringAndCheck(buf, fmt.Sprintf(`%% "%s" "%s" "%s" "%s" "%s" # NAME `, header.Title, header.Section, header.date, header.Source, header.Manual)) cobra.WriteStringAndCheck(buf, fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short)) cobra.WriteStringAndCheck(buf, "# SYNOPSIS\n") cobra.WriteStringAndCheck(buf, fmt.Sprintf("**%s**\n\n", cmd.UseLine())) cobra.WriteStringAndCheck(buf, "# DESCRIPTION\n") cobra.WriteStringAndCheck(buf, description+"\n\n") } // manPrintFlags prints the flags in a format suitable for a man page. // // It takes an io.StringWriter to write to and a pflag.FlagSet containing the flags to print. // For each flag, it checks if it is deprecated or hidden. If not, it formats the flag name, // shorthand, and usage information according to its type (string or other) and whether it has // a default value. The formatted string is then written to the provided writer. func manPrintFlags(buf io.StringWriter, 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" cobra.WriteStringAndCheck(buf, fmt.Sprintf(format, flag.DefValue, flag.Usage)) }) } // manPrintOptions writes the options for a command to a StringWriter. // It includes both non-inherited and inherited flags, if available. func manPrintOptions(buf io.StringWriter, command *cobra.Command) { flags := command.NonInheritedFlags() if flags.HasAvailableFlags() { cobra.WriteStringAndCheck(buf, "# OPTIONS\n") manPrintFlags(buf, flags) cobra.WriteStringAndCheck(buf, "\n") } flags = command.InheritedFlags() if flags.HasAvailableFlags() { cobra.WriteStringAndCheck(buf, "# OPTIONS INHERITED FROM PARENT COMMANDS\n") manPrintFlags(buf, flags) cobra.WriteStringAndCheck(buf, "\n") } } // genMan generates a man page for the given Cobra command and header. // It initializes default help commands and flags, processes the command path, // and constructs the man page content including preamble, options, examples, // see also sections, and history. // // Parameters: // - cmd: The Cobra command for which to generate the man page. // - header: Header information for the man page, such as section and date. // // Returns: // - A byte slice containing the generated man page content. func genMan(cmd *cobra.Command, header *GenManHeader) []byte { cmd.InitDefaultHelpCmd() cmd.InitDefaultHelpFlag() // something like `rootcmd-subcmd1-subcmd2` dashCommandName := strings.ReplaceAll(cmd.CommandPath(), " ", "-") 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.ReplaceAll(parentPath, " ", "-") 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() }