Skip to content

Commit

Permalink
created a tool to generate and install man pages
Browse files Browse the repository at this point in the history
Signed-off-by: Teodor Adrian Miron <[email protected]>
  • Loading branch information
teodoradriann committed Dec 18, 2024
1 parent f43135c commit 7bde584
Show file tree
Hide file tree
Showing 4 changed files with 335 additions and 1 deletion.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
github.com/containerd/nerdctl v1.7.7
github.com/containerd/platforms v0.2.1
github.com/containers/image/v5 v5.32.2
github.com/cpuguy83/go-md2man/v2 v2.0.5
github.com/cyphar/filepath-securejoin v0.3.5
github.com/dgraph-io/badger/v3 v3.2103.5
github.com/docker/cli v27.4.0+incompatible
Expand Down Expand Up @@ -233,6 +234,7 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rootless-containers/rootlesskit v1.1.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect
github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -359,10 +359,13 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
Expand Down Expand Up @@ -1059,6 +1062,7 @@ github.com/rootless-containers/rootlesskit v1.1.1 h1:F5psKWoWY9/VjZ3ifVcaosjvFZJ
github.com/rootless-containers/rootlesskit v1.1.1/go.mod h1:UD5GoA3dqKCJrnvnhVgQQnweMF2qZnf9KLw8EewcMZI=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
Expand Down
2 changes: 1 addition & 1 deletion tools/gendocs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,4 @@ type byName []*cobra.Command

func (s byName) Len() int { return len(s) }
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
328 changes: 328 additions & 0 deletions tools/genman/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2023, Unikraft GmbH, spf13/cobra, The Cobra Authors and KraftKit Authors.
// Licensed under the Apache-2.0 License (the "License").
// You may not use this file except in compliance with the License.

package main

import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"time"

kraft "kraftkit.sh/internal/cli/kraft"

"github.com/cpuguy83/go-md2man/v2/md2man"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

const (
MAN_DIR = "../../docs/man"
MAN_INSTALL_DIR = "/usr/local/share/man/man1"
)

func main() {
if len(os.Args[1:]) == 0 {
fmt.Printf("usage: %s [--man | --install] \n", os.Args[0])
os.Exit(1)
}

arg := os.Args[1]

switch arg {
case "--man", "-m":
if err := os.MkdirAll(MAN_DIR, 0o775); err != nil {
fmt.Println("could not create MAN_DIR:", err)
os.Exit(1)
}

cmd := kraft.NewCmd()
header := &GenManHeader{
Title: "KraftKit CLI",
Source: "KraftKit Documentation",
Manual: "KraftKit Manual",
}

fmt.Println("Generating man pages in directory kraftkit/docs/man")
if err := GenManTree(cmd, header, MAN_DIR); err != nil {
fmt.Println("error generating man pages:", err)
os.Exit(1)
}

fmt.Println("Man pages generated successfully in kraftkit/docs/man")

case "--install", "-i":
srcDir := MAN_DIR
destDir := MAN_INSTALL_DIR
if err := InstallManPages(srcDir, destDir); err != nil {
fmt.Printf("Error installing man pages: %v\n", err)
os.Exit(1)
}
fmt.Println("Man pages installed successfully!")
default:
fmt.Printf("usage: %s [--man | --install] \n", os.Args[0])
os.Exit(1)
}
}

func GenManTree(cmd *cobra.Command, header *GenManHeader, dir string) error {
return GenManTreeFromOpts(cmd, GenManTreeOptions{
Header: header,
Path: dir,
CommandSeparator: "-",
})
}

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)
}

type GenManTreeOptions struct {
Header *GenManHeader
Path string
CommandSeparator string
}

type GenManHeader struct {
Title string
Section string
Date *time.Time
date string
Source string
Manual string
}

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
}

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 KraftKit"
}
return nil
}

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")
}

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" {
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))
})
}

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")
}
}

func genMan(cmd *cobra.Command, header *GenManHeader) []byte {
cmd.InitDefaultHelpCmd()
cmd.InitDefaultHelpFlag()

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 KraftKit\n", header.Date.Format("2-Jan-2006")))
}
return buf.Bytes()
}

func hasSeeAlso(cmd *cobra.Command) bool {
if cmd.HasParent() {
return true
}
for _, c := range cmd.Commands() {
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
continue
}
return true
}
return false
}

func InstallManPages(srcDir string, destDir string) error {
if err := os.MkdirAll(destDir, 0o755); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}

files, err := os.ReadDir(srcDir)
if err != nil {
return fmt.Errorf("failed to read source directory: %w", err)
}

for _, file := range files {
if filepath.Ext(file.Name()) != ".1" {
continue
}

srcPath := filepath.Join(srcDir, file.Name())
destPath := filepath.Join(destDir, file.Name())

fmt.Printf("Installing %s -> %s\n", srcPath, destPath)
input, err := os.ReadFile(srcPath)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", srcPath, err)
}
if err := os.WriteFile(destPath, input, 0o644); err != nil {
return fmt.Errorf("failed to write file %s: %w", destPath, err)
}
}

fmt.Println("Updating man database...")
cmd := exec.Command("mandb")
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to update man database: %w", err)
}

return nil
}

type byName []*cobra.Command

func (s byName) Len() int { return len(s) }
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }

0 comments on commit 7bde584

Please sign in to comment.