Skip to content

Commit be388bf

Browse files
committed
add man support
Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
1 parent 5090452 commit be388bf

17 files changed

Lines changed: 490 additions & 185 deletions

clidocstool.go

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ package clidocstool
1717
import (
1818
"errors"
1919
"io"
20+
"log"
2021
"os"
22+
"path/filepath"
2123
"strings"
2224

2325
"github.com/spf13/cobra"
26+
"github.com/spf13/cobra/doc"
2427
)
2528

2629
// Options defines options for cli-docs-tool
@@ -29,6 +32,8 @@ type Options struct {
2932
SourceDir string
3033
TargetDir string
3134
Plugin bool
35+
36+
ManHeader *doc.GenManHeader
3237
}
3338

3439
// Client represents an active cli-docs-tool object
@@ -37,6 +42,8 @@ type Client struct {
3742
source string
3843
target string
3944
plugin bool
45+
46+
manHeader *doc.GenManHeader
4047
}
4148

4249
// New initializes a new cli-docs-tool client
@@ -48,9 +55,10 @@ func New(opts Options) (*Client, error) {
4855
return nil, errors.New("source dir required")
4956
}
5057
c := &Client{
51-
root: opts.Root,
52-
source: opts.SourceDir,
53-
plugin: opts.Plugin,
58+
root: opts.Root,
59+
source: opts.SourceDir,
60+
plugin: opts.Plugin,
61+
manHeader: opts.ManHeader,
5462
}
5563
if len(opts.TargetDir) == 0 {
5664
c.target = c.source
@@ -73,9 +81,69 @@ func (c *Client) GenAllTree() error {
7381
if err = c.GenYamlTree(c.root); err != nil {
7482
return err
7583
}
84+
if err = c.GenManTree(c.root); err != nil {
85+
return err
86+
}
7687
return nil
7788
}
7889

90+
// loadLongDescription gets long descriptions and examples from markdown.
91+
func (c *Client) loadLongDescription(parentCmd *cobra.Command, generator string) error {
92+
for _, cmd := range parentCmd.Commands() {
93+
if cmd.HasSubCommands() {
94+
if err := c.loadLongDescription(cmd, generator); err != nil {
95+
return err
96+
}
97+
}
98+
name := cmd.CommandPath()
99+
if i := strings.Index(name, " "); i >= 0 {
100+
// remove root command / binary name
101+
name = name[i+1:]
102+
}
103+
if name == "" {
104+
continue
105+
}
106+
mdFile := strings.ReplaceAll(name, " ", "_") + ".md"
107+
sourcePath := filepath.Join(c.source, mdFile)
108+
content, err := os.ReadFile(sourcePath)
109+
if os.IsNotExist(err) {
110+
log.Printf("WARN: %s does not exist, skipping Markdown examples for %s docs\n", mdFile, generator)
111+
continue
112+
}
113+
if err != nil {
114+
return err
115+
}
116+
applyDescriptionAndExamples(cmd, string(content))
117+
}
118+
return nil
119+
}
120+
121+
// applyDescriptionAndExamples fills in cmd.Long and cmd.Example with the
122+
// "Description" and "Examples" H2 sections in mdString (if present).
123+
func applyDescriptionAndExamples(cmd *cobra.Command, mdString string) {
124+
sections := getSections(mdString)
125+
var (
126+
anchors []string
127+
md string
128+
)
129+
if sections["description"] != "" {
130+
md, anchors = cleanupMarkDown(sections["description"])
131+
cmd.Long = md
132+
anchors = append(anchors, md)
133+
}
134+
if sections["examples"] != "" {
135+
md, anchors = cleanupMarkDown(sections["examples"])
136+
cmd.Example = md
137+
anchors = append(anchors, md)
138+
}
139+
if len(anchors) > 0 {
140+
if cmd.Annotations == nil {
141+
cmd.Annotations = make(map[string]string)
142+
}
143+
cmd.Annotations["anchors"] = strings.Join(anchors, ",")
144+
}
145+
}
146+
79147
func fileExists(f string) bool {
80148
info, err := os.Stat(f)
81149
if os.IsNotExist(err) {

clidocstool_man.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright 2016 cli-docs-tool authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package clidocstool
16+
17+
import (
18+
"fmt"
19+
"log"
20+
"os"
21+
"strconv"
22+
"time"
23+
24+
"github.com/spf13/cobra"
25+
"github.com/spf13/cobra/doc"
26+
)
27+
28+
// GenManTree generates a man page for the command and all descendants.
29+
// If SOURCE_DATE_EPOCH is set, in order to allow reproducible package
30+
// builds, we explicitly set the build time to SOURCE_DATE_EPOCH.
31+
func (c *Client) GenManTree(cmd *cobra.Command) error {
32+
if err := c.loadLongDescription(cmd, "man"); err != nil {
33+
return err
34+
}
35+
36+
if epoch := os.Getenv("SOURCE_DATE_EPOCH"); c.manHeader != nil && epoch != "" {
37+
unixEpoch, err := strconv.ParseInt(epoch, 10, 64)
38+
if err != nil {
39+
return fmt.Errorf("invalid SOURCE_DATE_EPOCH: %v", err)
40+
}
41+
now := time.Unix(unixEpoch, 0)
42+
c.manHeader.Date = &now
43+
}
44+
45+
return c.genManTreeCustom(cmd)
46+
}
47+
48+
func (c *Client) genManTreeCustom(cmd *cobra.Command) error {
49+
for _, sc := range cmd.Commands() {
50+
if err := c.genManTreeCustom(sc); err != nil {
51+
return err
52+
}
53+
}
54+
55+
// always disable the addition of [flags] to the usage
56+
cmd.DisableFlagsInUseLine = true
57+
58+
// always disable "spf13/cobra" auto gen tag
59+
cmd.DisableAutoGenTag = true
60+
61+
// Skip the root command altogether, to prevent generating a useless
62+
// md file for plugins.
63+
if c.plugin && !cmd.HasParent() {
64+
return nil
65+
}
66+
67+
log.Printf("INFO: Generating Man for %q", cmd.CommandPath())
68+
69+
return doc.GenManTreeFromOpts(cmd, doc.GenManTreeOptions{
70+
Header: c.manHeader,
71+
Path: c.target,
72+
CommandSeparator: "-",
73+
})
74+
}

clidocstool_man_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2024 cli-docs-tool authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package clidocstool
16+
17+
import (
18+
"io/fs"
19+
"os"
20+
"path"
21+
"path/filepath"
22+
"regexp"
23+
"strconv"
24+
"testing"
25+
"time"
26+
27+
"github.com/spf13/cobra/doc"
28+
"github.com/stretchr/testify/assert"
29+
"github.com/stretchr/testify/require"
30+
)
31+
32+
//nolint:errcheck
33+
func TestGenManTree(t *testing.T) {
34+
setup()
35+
tmpdir := t.TempDir()
36+
37+
epoch, err := time.Parse("2006-Jan-02", "2020-Jan-10")
38+
require.NoError(t, err)
39+
t.Setenv("SOURCE_DATE_EPOCH", strconv.FormatInt(epoch.Unix(), 10))
40+
41+
require.NoError(t, copyFile(path.Join("fixtures", "buildx_stop.pre.md"), path.Join(tmpdir, "buildx_stop.md")))
42+
43+
c, err := New(Options{
44+
Root: buildxCmd,
45+
SourceDir: tmpdir,
46+
Plugin: true,
47+
ManHeader: &doc.GenManHeader{
48+
Title: "DOCKER",
49+
Section: "1",
50+
Source: "Docker Community",
51+
Manual: "Docker User Manuals",
52+
},
53+
})
54+
require.NoError(t, err)
55+
require.NoError(t, c.GenManTree(buildxCmd))
56+
57+
seen := make(map[string]struct{})
58+
remanpage := regexp.MustCompile(`\.\d+$`)
59+
60+
filepath.Walk("fixtures", func(path string, info fs.FileInfo, err error) error {
61+
fname := filepath.Base(path)
62+
// ignore dirs and any file that is not a manpage
63+
if info.IsDir() || !remanpage.MatchString(fname) {
64+
return nil
65+
}
66+
t.Run(fname, func(t *testing.T) {
67+
seen[fname] = struct{}{}
68+
require.NoError(t, err)
69+
70+
bres, err := os.ReadFile(filepath.Join(tmpdir, fname))
71+
require.NoError(t, err)
72+
73+
bexc, err := os.ReadFile(path)
74+
require.NoError(t, err)
75+
assert.Equal(t, string(bexc), string(bres))
76+
})
77+
return nil
78+
})
79+
80+
filepath.Walk(tmpdir, func(path string, info fs.FileInfo, err error) error {
81+
fname := filepath.Base(path)
82+
// ignore dirs and any file that is not a manpage
83+
if info.IsDir() || !remanpage.MatchString(fname) {
84+
return nil
85+
}
86+
t.Run("seen_"+fname, func(t *testing.T) {
87+
if _, ok := seen[fname]; !ok {
88+
t.Errorf("file %s not found in fixtures", fname)
89+
}
90+
})
91+
return nil
92+
})
93+
}

clidocstool_md_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727

2828
//nolint:errcheck
2929
func TestGenMarkdownTree(t *testing.T) {
30+
setup()
3031
tmpdir := t.TempDir()
3132

3233
require.NoError(t, copyFile(path.Join("fixtures", "buildx_stop.pre.md"), path.Join(tmpdir, "buildx_stop.md")))

clidocstool_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ import (
1919
"os"
2020
"path"
2121
"path/filepath"
22+
"strconv"
2223
"strings"
2324
"testing"
25+
"time"
2426

2527
"github.com/docker/cli-docs-tool/annotation"
2628
"github.com/spf13/cobra"
29+
"github.com/spf13/cobra/doc"
2730
"github.com/stretchr/testify/assert"
2831
"github.com/stretchr/testify/require"
2932
)
@@ -37,7 +40,7 @@ var (
3740
)
3841

3942
//nolint:errcheck
40-
func init() {
43+
func setup() {
4144
dockerCmd = &cobra.Command{
4245
Use: "docker [OPTIONS] COMMAND [ARG...]",
4346
Short: "A self-sufficient runtime for containers",
@@ -192,14 +195,25 @@ format: "default|<id>[=<socket>|<key>[,<key>]]"`)
192195

193196
//nolint:errcheck
194197
func TestGenAllTree(t *testing.T) {
198+
setup()
195199
tmpdir := t.TempDir()
196200

201+
epoch, err := time.Parse("2006-Jan-02", "2020-Jan-10")
202+
require.NoError(t, err)
203+
t.Setenv("SOURCE_DATE_EPOCH", strconv.FormatInt(epoch.Unix(), 10))
204+
197205
require.NoError(t, copyFile(path.Join("fixtures", "buildx_stop.pre.md"), path.Join(tmpdir, "buildx_stop.md")))
198206

199207
c, err := New(Options{
200208
Root: buildxCmd,
201209
SourceDir: tmpdir,
202210
Plugin: true,
211+
ManHeader: &doc.GenManHeader{
212+
Title: "DOCKER",
213+
Section: "1",
214+
Source: "Docker Community",
215+
Manual: "Docker User Manuals",
216+
},
203217
})
204218
require.NoError(t, err)
205219
require.NoError(t, c.GenAllTree())

0 commit comments

Comments
 (0)