Skip to content

Commit d8d8f3e

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

16 files changed

+429
-189
lines changed

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: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2021 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+
"os"
19+
"path"
20+
"path/filepath"
21+
"strconv"
22+
"testing"
23+
"time"
24+
25+
"github.com/spf13/cobra/doc"
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
)
29+
30+
//nolint:errcheck
31+
func TestGenManTree(t *testing.T) {
32+
setup()
33+
tmpdir := t.TempDir()
34+
35+
epoch, err := time.Parse("2006-Jan-02", "2020-Jan-10")
36+
require.NoError(t, err)
37+
t.Setenv("SOURCE_DATE_EPOCH", strconv.FormatInt(epoch.Unix(), 10))
38+
39+
require.NoError(t, copyFile(path.Join("fixtures", "buildx_stop.pre.md"), path.Join(tmpdir, "buildx_stop.md")))
40+
41+
c, err := New(Options{
42+
Root: buildxCmd,
43+
SourceDir: tmpdir,
44+
Plugin: true,
45+
ManHeader: &doc.GenManHeader{
46+
Title: "DOCKER",
47+
Section: "1",
48+
Source: "Docker Community",
49+
Manual: "Docker User Manuals",
50+
},
51+
})
52+
require.NoError(t, err)
53+
require.NoError(t, c.GenManTree(buildxCmd))
54+
55+
for _, tt := range []string{"docker-buildx.1", "docker-buildx-build.1", "docker-buildx-stop.1"} {
56+
tt := tt
57+
t.Run(tt, func(t *testing.T) {
58+
bres, err := os.ReadFile(filepath.Join(tmpdir, tt))
59+
require.NoError(t, err)
60+
61+
bexc, err := os.ReadFile(path.Join("fixtures", tt))
62+
require.NoError(t, err)
63+
assert.Equal(t, string(bexc), string(bres))
64+
})
65+
}
66+
}

clidocstool_md_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ import (
2626

2727
//nolint:errcheck
2828
func TestGenMarkdownTree(t *testing.T) {
29+
setup()
2930
tmpdir := t.TempDir()
3031

31-
err := copyFile(path.Join("fixtures", "buildx_stop.pre.md"), path.Join(tmpdir, "buildx_stop.md"))
32-
require.NoError(t, err)
32+
require.NoError(t, copyFile(path.Join("fixtures", "buildx_stop.pre.md"), path.Join(tmpdir, "buildx_stop.md")))
3333

3434
c, err := New(Options{
3535
Root: buildxCmd,

clidocstool_test.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ import (
1818
"os"
1919
"path"
2020
"path/filepath"
21+
"strconv"
2122
"testing"
23+
"time"
2224

2325
"github.com/docker/cli-docs-tool/annotation"
2426
"github.com/spf13/cobra"
27+
"github.com/spf13/cobra/doc"
2528
"github.com/stretchr/testify/assert"
2629
"github.com/stretchr/testify/require"
2730
)
@@ -34,7 +37,7 @@ var (
3437
)
3538

3639
//nolint:errcheck
37-
func init() {
40+
func setup() {
3841
dockerCmd = &cobra.Command{
3942
Use: "docker [OPTIONS] COMMAND [ARG...]",
4043
Short: "A self-sufficient runtime for containers",
@@ -178,20 +181,30 @@ func init() {
178181

179182
//nolint:errcheck
180183
func TestGenAllTree(t *testing.T) {
184+
setup()
181185
tmpdir := t.TempDir()
182186

183-
err := copyFile(path.Join("fixtures", "buildx_stop.pre.md"), path.Join(tmpdir, "buildx_stop.md"))
187+
epoch, err := time.Parse("2006-Jan-02", "2020-Jan-10")
184188
require.NoError(t, err)
189+
t.Setenv("SOURCE_DATE_EPOCH", strconv.FormatInt(epoch.Unix(), 10))
190+
191+
require.NoError(t, copyFile(path.Join("fixtures", "buildx_stop.pre.md"), path.Join(tmpdir, "buildx_stop.md")))
185192

186193
c, err := New(Options{
187194
Root: buildxCmd,
188195
SourceDir: tmpdir,
189196
Plugin: true,
197+
ManHeader: &doc.GenManHeader{
198+
Title: "DOCKER",
199+
Section: "1",
200+
Source: "Docker Community",
201+
Manual: "Docker User Manuals",
202+
},
190203
})
191204
require.NoError(t, err)
192205
require.NoError(t, c.GenAllTree())
193206

194-
for _, tt := range []string{"buildx.md", "buildx_build.md", "buildx_stop.md", "docker_buildx.yaml", "docker_buildx_build.yaml", "docker_buildx_stop.yaml"} {
207+
for _, tt := range []string{"buildx.md", "buildx_build.md", "buildx_stop.md", "docker_buildx.yaml", "docker_buildx_build.yaml", "docker_buildx_stop.yaml", "docker-buildx.1", "docker-buildx-build.1", "docker-buildx-stop.1"} {
195208
tt := tt
196209
t.Run(tt, func(t *testing.T) {
197210
bres, err := os.ReadFile(filepath.Join(tmpdir, tt))

clidocstool_yaml.go

Lines changed: 1 addition & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ type cmdDoc struct {
7777
// it is undefined which help output will be in the file `cmd-sub-third.1`.
7878
func (c *Client) GenYamlTree(cmd *cobra.Command) error {
7979
emptyStr := func(s string) string { return "" }
80-
if err := c.loadLongDescription(cmd); err != nil {
80+
if err := c.loadLongDescription(cmd, "yaml"); err != nil {
8181
return err
8282
}
8383
return c.genYamlTreeCustom(cmd, emptyStr)
@@ -369,63 +369,6 @@ func hasSeeAlso(cmd *cobra.Command) bool {
369369
return false
370370
}
371371

372-
// loadLongDescription gets long descriptions and examples from markdown.
373-
func (c *Client) loadLongDescription(parentCmd *cobra.Command) error {
374-
for _, cmd := range parentCmd.Commands() {
375-
if cmd.HasSubCommands() {
376-
if err := c.loadLongDescription(cmd); err != nil {
377-
return err
378-
}
379-
}
380-
name := cmd.CommandPath()
381-
if i := strings.Index(name, " "); i >= 0 {
382-
// remove root command / binary name
383-
name = name[i+1:]
384-
}
385-
if name == "" {
386-
continue
387-
}
388-
mdFile := strings.ReplaceAll(name, " ", "_") + ".md"
389-
sourcePath := filepath.Join(c.source, mdFile)
390-
content, err := os.ReadFile(sourcePath)
391-
if os.IsNotExist(err) {
392-
log.Printf("WARN: %s does not exist, skipping Markdown examples for YAML doc\n", mdFile)
393-
continue
394-
}
395-
if err != nil {
396-
return err
397-
}
398-
applyDescriptionAndExamples(cmd, string(content))
399-
}
400-
return nil
401-
}
402-
403-
// applyDescriptionAndExamples fills in cmd.Long and cmd.Example with the
404-
// "Description" and "Examples" H2 sections in mdString (if present).
405-
func applyDescriptionAndExamples(cmd *cobra.Command, mdString string) {
406-
sections := getSections(mdString)
407-
var (
408-
anchors []string
409-
md string
410-
)
411-
if sections["description"] != "" {
412-
md, anchors = cleanupMarkDown(sections["description"])
413-
cmd.Long = md
414-
anchors = append(anchors, md)
415-
}
416-
if sections["examples"] != "" {
417-
md, anchors = cleanupMarkDown(sections["examples"])
418-
cmd.Example = md
419-
anchors = append(anchors, md)
420-
}
421-
if len(anchors) > 0 {
422-
if cmd.Annotations == nil {
423-
cmd.Annotations = make(map[string]string)
424-
}
425-
cmd.Annotations["anchors"] = strings.Join(anchors, ",")
426-
}
427-
}
428-
429372
type byName []*cobra.Command
430373

431374
func (s byName) Len() int { return len(s) }

0 commit comments

Comments
 (0)