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