Skip to content

Commit 5056ef1

Browse files
teodoradrianncraciunoiuc
authored andcommitted
feat(tools): Add man pages generator
This is an initial version of the auto-generator of man pages for kraftkit. This parses help messages and generates man files which can be afterwards installed. To generate them, use the new `make` command `make man`. Signed-off-by: Teodor Adrian Miron <[email protected]> Signed-off-by: Cezar Craciunoiu <[email protected]>
1 parent d18a23b commit 5056ef1

File tree

4 files changed

+339
-1
lines changed

4 files changed

+339
-1
lines changed

Makefile

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,12 @@ properclean: ## Completely clean the repository's build artifacts.
238238
.PHONY: docs
239239
docs: OUTDIR ?= $(WORKDIR)/docs/
240240
docs: ## Generate Markdown documentation.
241-
$(GO) run $(WORKDIR)/tools/gendocs $(OUTDIR)
241+
$(GO) run -tags "containers_image_storage_stub,containers_image_openpgp" $(WORKDIR)/tools/gendocs $(OUTDIR)
242+
243+
.PHONY: man
244+
man: OUTDIR ?= $(WORKDIR)/docs/man/
245+
man: ## Generate manpage documentation.
246+
$(GO) run -tags "containers_image_storage_stub,containers_image_openpgp" $(WORKDIR)/tools/genman generate $(OUTDIR)
242247

243248
.PHONY: help
244249
help: ## Show this help menu and exit.

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require (
2626
github.com/containerd/nerdctl v1.7.7
2727
github.com/containerd/platforms v0.2.1
2828
github.com/containers/image/v5 v5.32.2
29+
github.com/cpuguy83/go-md2man/v2 v2.0.6
2930
github.com/cyphar/filepath-securejoin v0.3.6
3031
github.com/dgraph-io/badger/v3 v3.2103.5
3132
github.com/docker/cli v27.5.0+incompatible
@@ -233,6 +234,7 @@ require (
233234
github.com/prometheus/procfs v0.15.1 // indirect
234235
github.com/rivo/uniseg v0.4.7 // indirect
235236
github.com/rootless-containers/rootlesskit v1.1.1 // indirect
237+
github.com/russross/blackfriday/v2 v2.1.0 // indirect
236238
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect
237239
github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
238240
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc
363363
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
364364
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
365365
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
366+
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
367+
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
366368
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
367369
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
368370
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@@ -1060,6 +1062,7 @@ github.com/rootless-containers/rootlesskit v1.1.1 h1:F5psKWoWY9/VjZ3ifVcaosjvFZJ
10601062
github.com/rootless-containers/rootlesskit v1.1.1/go.mod h1:UD5GoA3dqKCJrnvnhVgQQnweMF2qZnf9KLw8EewcMZI=
10611063
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
10621064
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
1065+
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
10631066
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
10641067
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
10651068
github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=

tools/genman/main.go

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright (c) 2025, Unikraft GmbH, spf13/cobra, The Cobra Authors and KraftKit Authors.
3+
// Licensed under the Apache-2.0 License (the "License").
4+
// You may not use this file except in compliance with the License.
5+
6+
package main
7+
8+
import (
9+
"bytes"
10+
"compress/gzip"
11+
"fmt"
12+
"io"
13+
"os"
14+
"os/exec"
15+
"path/filepath"
16+
"sort"
17+
"strconv"
18+
"strings"
19+
"time"
20+
21+
kraft "kraftkit.sh/internal/cli/kraft"
22+
23+
"github.com/cpuguy83/go-md2man/v2/md2man"
24+
"github.com/spf13/cobra"
25+
"github.com/spf13/pflag"
26+
)
27+
28+
func main() {
29+
if len(os.Args[1:]) < 2 || len(os.Args[1:]) > 3 {
30+
fmt.Printf("usage: %s generate | (install src) dest\n", os.Args[0])
31+
os.Exit(1)
32+
}
33+
34+
switch os.Args[1] {
35+
case "generate":
36+
destDir := os.Args[2]
37+
if err := os.MkdirAll(destDir, 0o775); err != nil {
38+
fmt.Println("could not create directory to generate files: ", err)
39+
os.Exit(1)
40+
}
41+
42+
cmd := kraft.NewCmd()
43+
header := &GenManHeader{
44+
Title: "KraftKit CLI",
45+
Source: "KraftKit Documentation",
46+
Manual: "KraftKit Manual",
47+
}
48+
49+
fmt.Println("Generating man pages in directory ", destDir)
50+
if err := GenManTree(cmd, header, destDir); err != nil {
51+
fmt.Println("error generating man pages: ", err)
52+
os.Exit(1)
53+
}
54+
55+
case "install":
56+
srcDir := os.Args[2]
57+
destDir := os.Args[3]
58+
if err := InstallManPages(srcDir, destDir); err != nil {
59+
fmt.Println("Error installing man pages: ", err)
60+
os.Exit(1)
61+
}
62+
fmt.Println("Man pages installed successfully!")
63+
default:
64+
fmt.Printf("usage: %s generate | (install src) dest\n", os.Args[0])
65+
os.Exit(1)
66+
}
67+
}
68+
69+
func GenManTree(cmd *cobra.Command, header *GenManHeader, dir string) error {
70+
return GenManTreeFromOpts(cmd, GenManTreeOptions{
71+
Header: header,
72+
Path: dir,
73+
CommandSeparator: "-",
74+
})
75+
}
76+
77+
func GenManTreeFromOpts(cmd *cobra.Command, opts GenManTreeOptions) error {
78+
header := opts.Header
79+
if header == nil {
80+
header = &GenManHeader{}
81+
}
82+
for _, c := range cmd.Commands() {
83+
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
84+
continue
85+
}
86+
if err := GenManTreeFromOpts(c, opts); err != nil {
87+
return err
88+
}
89+
}
90+
section := "1"
91+
if header.Section != "" {
92+
section = header.Section
93+
}
94+
95+
separator := "_"
96+
if opts.CommandSeparator != "" {
97+
separator = opts.CommandSeparator
98+
}
99+
basename := strings.ReplaceAll(cmd.CommandPath(), " ", separator)
100+
filename := filepath.Join(opts.Path, basename+"."+section+".gz")
101+
f, err := os.Create(filename)
102+
if err != nil {
103+
return err
104+
}
105+
defer f.Close()
106+
107+
gzf, err := gzip.NewWriterLevel(f, gzip.BestCompression)
108+
if err != nil {
109+
return err
110+
}
111+
defer gzf.Close()
112+
113+
headerCopy := *header
114+
return GenMan(cmd, &headerCopy, gzf)
115+
}
116+
117+
type GenManTreeOptions struct {
118+
Header *GenManHeader
119+
Path string
120+
CommandSeparator string
121+
}
122+
123+
type GenManHeader struct {
124+
Title string
125+
Section string
126+
Date *time.Time
127+
date string
128+
Source string
129+
Manual string
130+
}
131+
132+
func GenMan(cmd *cobra.Command, header *GenManHeader, w io.Writer) error {
133+
if header == nil {
134+
header = &GenManHeader{}
135+
}
136+
if err := fillHeader(header, cmd.CommandPath(), cmd.DisableAutoGenTag); err != nil {
137+
return err
138+
}
139+
140+
b := genMan(cmd, header)
141+
_, err := w.Write(md2man.Render(b))
142+
return err
143+
}
144+
145+
func fillHeader(header *GenManHeader, name string, disableAutoGen bool) error {
146+
if header.Title == "" {
147+
header.Title = strings.ToUpper(strings.ReplaceAll(name, " ", "\\-"))
148+
}
149+
if header.Section == "" {
150+
header.Section = "1"
151+
}
152+
if header.Date == nil {
153+
now := time.Now()
154+
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); epoch != "" {
155+
unixEpoch, err := strconv.ParseInt(epoch, 10, 64)
156+
if err != nil {
157+
return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err)
158+
}
159+
now = time.Unix(unixEpoch, 0)
160+
}
161+
header.Date = &now
162+
}
163+
header.date = header.Date.Format("Jan 2006")
164+
if header.Source == "" && !disableAutoGen {
165+
header.Source = "Auto generated by KraftKit"
166+
}
167+
return nil
168+
}
169+
170+
func manPreamble(buf io.StringWriter, header *GenManHeader, cmd *cobra.Command, dashedName string) {
171+
description := cmd.Long
172+
if len(description) == 0 {
173+
description = cmd.Short
174+
}
175+
176+
cobra.WriteStringAndCheck(buf, fmt.Sprintf(`%% "%s" "%s" "%s" "%s" "%s"
177+
# NAME
178+
`, header.Title, header.Section, header.date, header.Source, header.Manual))
179+
cobra.WriteStringAndCheck(buf, fmt.Sprintf("%s \\- %s\n\n", dashedName, cmd.Short))
180+
cobra.WriteStringAndCheck(buf, "# SYNOPSIS\n")
181+
cobra.WriteStringAndCheck(buf, fmt.Sprintf("**%s**\n\n", cmd.UseLine()))
182+
cobra.WriteStringAndCheck(buf, "# DESCRIPTION\n")
183+
cobra.WriteStringAndCheck(buf, description+"\n\n")
184+
}
185+
186+
func manPrintFlags(buf io.StringWriter, flags *pflag.FlagSet) {
187+
flags.VisitAll(func(flag *pflag.Flag) {
188+
if len(flag.Deprecated) > 0 || flag.Hidden {
189+
return
190+
}
191+
format := ""
192+
if len(flag.Shorthand) > 0 && len(flag.ShorthandDeprecated) == 0 {
193+
format = fmt.Sprintf("**-%s**, **--%s**", flag.Shorthand, flag.Name)
194+
} else {
195+
format = fmt.Sprintf("**--%s**", flag.Name)
196+
}
197+
if len(flag.NoOptDefVal) > 0 {
198+
format += "["
199+
}
200+
if flag.Value.Type() == "string" {
201+
format += "=%q"
202+
} else {
203+
format += "=%s"
204+
}
205+
if len(flag.NoOptDefVal) > 0 {
206+
format += "]"
207+
}
208+
format += "\n\n\t\t%s\n\n"
209+
cobra.WriteStringAndCheck(buf, fmt.Sprintf(format, flag.DefValue, flag.Usage))
210+
})
211+
}
212+
213+
func manPrintOptions(buf io.StringWriter, command *cobra.Command) {
214+
flags := command.NonInheritedFlags()
215+
if flags.HasAvailableFlags() {
216+
cobra.WriteStringAndCheck(buf, "# OPTIONS\n")
217+
manPrintFlags(buf, flags)
218+
}
219+
flags = command.InheritedFlags()
220+
if flags.HasAvailableFlags() {
221+
cobra.WriteStringAndCheck(buf, "# OPTIONS INHERITED FROM PARENT COMMANDS\n")
222+
manPrintFlags(buf, flags)
223+
}
224+
}
225+
226+
func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
227+
cmd.InitDefaultHelpCmd()
228+
cmd.InitDefaultHelpFlag()
229+
230+
dashCommandName := strings.ReplaceAll(cmd.CommandPath(), " ", "-")
231+
232+
buf := new(bytes.Buffer)
233+
234+
manPreamble(buf, header, cmd, dashCommandName)
235+
manPrintOptions(buf, cmd)
236+
if len(cmd.Example) > 0 {
237+
buf.WriteString("# EXAMPLES\n")
238+
cmd.Example = "\t" + cmd.Example
239+
cmd.Example = strings.ReplaceAll(cmd.Example, "\n", "\n\t")
240+
cmd.Example = strings.ReplaceAll(cmd.Example, "\\\n", "\\\n\n\t")
241+
buf.WriteString(fmt.Sprintf("\n%s\n\n", cmd.Example))
242+
}
243+
if hasSeeAlso(cmd) {
244+
buf.WriteString("# SEE ALSO\n")
245+
seealsos := make([]string, 0)
246+
if cmd.HasParent() {
247+
parentPath := cmd.Parent().CommandPath()
248+
dashParentPath := strings.ReplaceAll(parentPath, " ", "-")
249+
seealso := fmt.Sprintf("**%s(%s)**", dashParentPath, header.Section)
250+
seealsos = append(seealsos, seealso)
251+
cmd.VisitParents(func(c *cobra.Command) {
252+
if c.DisableAutoGenTag {
253+
cmd.DisableAutoGenTag = c.DisableAutoGenTag
254+
}
255+
})
256+
}
257+
children := cmd.Commands()
258+
sort.Sort(byName(children))
259+
for _, c := range children {
260+
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
261+
continue
262+
}
263+
seealso := fmt.Sprintf("**%s-%s(%s)**", dashCommandName, c.Name(), header.Section)
264+
seealsos = append(seealsos, seealso)
265+
}
266+
buf.WriteString(strings.Join(seealsos, ", ") + "\n")
267+
}
268+
if !cmd.DisableAutoGenTag {
269+
buf.WriteString(fmt.Sprintf("# HISTORY\n%s Auto generated by KraftKit\n", header.Date.Format("2-Jan-2006")))
270+
}
271+
return buf.Bytes()
272+
}
273+
274+
func hasSeeAlso(cmd *cobra.Command) bool {
275+
if cmd.HasParent() {
276+
return true
277+
}
278+
for _, c := range cmd.Commands() {
279+
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
280+
continue
281+
}
282+
return true
283+
}
284+
return false
285+
}
286+
287+
func InstallManPages(srcDir string, destDir string) error {
288+
if err := os.MkdirAll(destDir, 0o755); err != nil {
289+
return fmt.Errorf("failed to create destination directory: %w", err)
290+
}
291+
292+
files, err := os.ReadDir(srcDir)
293+
if err != nil {
294+
return fmt.Errorf("failed to read source directory: %w", err)
295+
}
296+
297+
for _, file := range files {
298+
if filepath.Ext(file.Name()) != ".1" {
299+
continue
300+
}
301+
302+
srcPath := filepath.Join(srcDir, file.Name())
303+
destPath := filepath.Join(destDir, file.Name())
304+
305+
fmt.Printf("Installing %s -> %s\n", srcPath, destPath)
306+
input, err := os.ReadFile(srcPath)
307+
if err != nil {
308+
return fmt.Errorf("failed to read file %s: %w", srcPath, err)
309+
}
310+
if err := os.WriteFile(destPath, input, 0o644); err != nil {
311+
return fmt.Errorf("failed to write file %s: %w", destPath, err)
312+
}
313+
}
314+
315+
fmt.Println("Updating man database...")
316+
cmd := exec.Command("mandb")
317+
if err := cmd.Run(); err != nil {
318+
return fmt.Errorf("failed to update man database: %w", err)
319+
}
320+
321+
return nil
322+
}
323+
324+
type byName []*cobra.Command
325+
326+
func (s byName) Len() int { return len(s) }
327+
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
328+
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }

0 commit comments

Comments
 (0)