github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/internal/docs/man.go (about) 1 package docs 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/cpuguy83/go-md2man/v2/md2man" 14 "github.com/spf13/cobra" 15 "github.com/spf13/pflag" 16 ) 17 18 // GenManTree will generate a man page for this command and all descendants 19 // in the directory given. The header may be nil. This function may not work 20 // correctly if your command names have `-` in them. If you have `cmd` with two 21 // subcmds, `sub` and `sub-third`, and `sub` has a subcommand called `third` 22 // it is undefined which help output will be in the file `cmd-sub-third.1`. 23 func GenManTree(cmd *cobra.Command, dir string) error { 24 return GenManTreeFromOpts(cmd, GenManTreeOptions{ 25 Path: dir, 26 CommandSeparator: "-", 27 }) 28 } 29 30 // GenManTreeFromOpts generates a man page for the command and all descendants. 31 // The pages are written to the opts.Path directory. 32 func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error { 33 for _, c := range cmd.Commands() { 34 if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { 35 continue 36 } 37 if err := GenManTreeFromOpts(c, opts); err != nil { 38 return err 39 } 40 } 41 42 section := "1" 43 separator := "_" 44 if opts.CommandSeparator != "" { 45 separator = opts.CommandSeparator 46 } 47 basename := strings.Replace(cmd.CommandPath(), " ", separator, -1) 48 filename := filepath.Join(opts.Path, basename+"."+section) 49 f, err := os.Create(filename) 50 if err != nil { 51 return err 52 } 53 defer f.Close() 54 55 var versionString string 56 if v := os.Getenv("GH_VERSION"); v != "" { 57 versionString = "GitHub CLI " + v 58 } 59 60 return GenMan(cmd, &GenManHeader{ 61 Section: section, 62 Source: versionString, 63 Manual: "GitHub CLI manual", 64 }, f) 65 } 66 67 // GenManTreeOptions is the options for generating the man pages. 68 // Used only in GenManTreeFromOpts. 69 type GenManTreeOptions struct { 70 Path string 71 CommandSeparator string 72 } 73 74 // GenManHeader is a lot like the .TH header at the start of man pages. These 75 // include the title, section, date, source, and manual. We will use the 76 // current time if Date is unset. 77 type GenManHeader struct { 78 Title string 79 Section string 80 Date *time.Time 81 Source string 82 Manual string 83 } 84 85 // GenMan will generate a man page for the given command and write it to 86 // w. The header argument may be nil, however obviously w may not. 87 func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error { 88 if err := fillHeader(header, cmd.CommandPath()); err != nil { 89 return err 90 } 91 92 b := genMan(cmd, header) 93 94 _, err := w.Write(md2man.Render(b)) 95 return err 96 } 97 98 func fillHeader(header *GenManHeader, name string) error { 99 if header.Title == "" { 100 header.Title = strings.ToUpper(strings.Replace(name, " ", "\\-", -1)) 101 } 102 if header.Section == "" { 103 header.Section = "1" 104 } 105 if header.Date == nil { 106 now := time.Now() 107 if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" { 108 unixEpoch, err := strconv.ParseInt(epoch, 10, 64) 109 if err != nil { 110 return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err) 111 } 112 now = time.Unix(unixEpoch, 0) 113 } 114 header.Date = &now 115 } 116 return nil 117 } 118 119 func manPreamble(buf *bytes.Buffer, header *GenManHeader, cmd *cobra.Command, dashedName string) { 120 buf.WriteString(fmt.Sprintf(`%% "%s" "%s" "%s" "%s" "%s" 121 # NAME 122 `, header.Title, header.Section, header.Date.Format("Jan 2006"), header.Source, header.Manual)) 123 buf.WriteString(fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short)) 124 buf.WriteString("# SYNOPSIS\n") 125 buf.WriteString(fmt.Sprintf("`%s`\n\n", cmd.UseLine())) 126 127 if cmd.Long != "" && cmd.Long != cmd.Short { 128 buf.WriteString("# DESCRIPTION\n") 129 buf.WriteString(cmd.Long + "\n\n") 130 } 131 } 132 133 func manPrintFlags(buf *bytes.Buffer, flags *pflag.FlagSet) { 134 flags.VisitAll(func(flag *pflag.Flag) { 135 if len(flag.Deprecated) > 0 || flag.Hidden || flag.Name == "help" { 136 return 137 } 138 varname, usage := pflag.UnquoteUsage(flag) 139 if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 { 140 buf.WriteString(fmt.Sprintf("`-%s`, `--%s`", flag.Shorthand, flag.Name)) 141 } else { 142 buf.WriteString(fmt.Sprintf("`--%s`", flag.Name)) 143 } 144 if varname == "" { 145 buf.WriteString("\n") 146 } else { 147 buf.WriteString(fmt.Sprintf(" `<%s>`\n", varname)) 148 } 149 buf.WriteString(fmt.Sprintf(": %s\n\n", usage)) 150 }) 151 } 152 153 func manPrintOptions(buf *bytes.Buffer, command *cobra.Command) { 154 flags := command.NonInheritedFlags() 155 if flags.HasAvailableFlags() { 156 buf.WriteString("# OPTIONS\n") 157 manPrintFlags(buf, flags) 158 buf.WriteString("\n") 159 } 160 flags = command.InheritedFlags() 161 if hasNonHelpFlags(flags) { 162 buf.WriteString("# OPTIONS INHERITED FROM PARENT COMMANDS\n") 163 manPrintFlags(buf, flags) 164 buf.WriteString("\n") 165 } 166 } 167 168 func genMan(cmd *cobra.Command, header *GenManHeader) []byte { 169 cmd.InitDefaultHelpCmd() 170 cmd.InitDefaultHelpFlag() 171 172 // something like `rootcmd-subcmd1-subcmd2` 173 dashCommandName := strings.Replace(cmd.CommandPath(), " ", "-", -1) 174 175 buf := new(bytes.Buffer) 176 177 manPreamble(buf, header, cmd, dashCommandName) 178 for _, g := range subcommandGroups(cmd) { 179 if len(g.Commands) == 0 { 180 continue 181 } 182 fmt.Fprintf(buf, "# %s\n", strings.ToUpper(g.Name)) 183 for _, subcmd := range g.Commands { 184 fmt.Fprintf(buf, "`%s`\n: %s\n\n", manLink(subcmd), subcmd.Short) 185 } 186 } 187 manPrintOptions(buf, cmd) 188 if len(cmd.Example) > 0 { 189 buf.WriteString("# EXAMPLE\n") 190 buf.WriteString(fmt.Sprintf("```\n%s\n```\n", cmd.Example)) 191 } 192 if cmd.HasParent() { 193 buf.WriteString("# SEE ALSO\n") 194 buf.WriteString(fmt.Sprintf("`%s`\n", manLink(cmd.Parent()))) 195 } 196 return buf.Bytes() 197 } 198 199 func manLink(cmd *cobra.Command) string { 200 p := cmd.CommandPath() 201 return fmt.Sprintf("%s(%d)", strings.Replace(p, " ", "-", -1), 1) 202 }