switch to cli-docs-tool for yaml docs generation

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
switch to cli-docs-tool and validate yamldocs

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax 2022-02-25 17:11:04 +01:00
parent 76a2a1945f
commit a650f4ddd0
No known key found for this signature in database
GPG Key ID: 3248E46B6BB8C7F7
16 changed files with 162 additions and 683 deletions

View File

@ -2,8 +2,11 @@
/cli/winresources/versioninfo.json
/cli/winresources/*.syso
/man/man*/
/docs/yaml/gen/
/docs/yaml/
/docs/vendor/
/docs/go.sum
profile.out
# top-level go.mod is not meant to be checked in
/go.mod
/go.sum

View File

@ -32,3 +32,21 @@ jobs:
uses: docker/bake-action@v1
with:
targets: ${{ matrix.target }}
validate-make:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target:
- yamldocs # ensure yamldocs target runs fine
steps:
-
name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Run
run: |
make -f docker.Makefile ${{ matrix.target }}

2
.gitignore vendored
View File

@ -13,8 +13,8 @@ Thumbs.db
/man/man1/
/man/man5/
/man/man8/
/docs/yaml/gen/
profile.out
# top-level go.mod is not meant to be checked in
/go.mod
/go.sum

View File

@ -11,7 +11,7 @@ _:=$(shell ./scripts/warn-outside-container $(MAKECMDGOALS))
.PHONY: clean
clean: ## remove build artifacts
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml/gen
rm -rf ./build/* cli/winresources/rsrc_* ./man/man[1-9] docs/yaml
.PHONY: test
test: test-unit ## run tests

3
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/vendor
/yaml
/go.sum

View File

@ -28,3 +28,9 @@ the place to edit them.
The docs in the general repo are open-source and we appreciate
your feedback and pull requests!
# Generate docs
```shell
$ make -f docker.Makefile yamldocs
```

67
docs/generate.go Normal file
View File

@ -0,0 +1,67 @@
// This file is intended for use with "go run"; it isn't really part of the package.
// +build docsgen
package main
import (
"log"
"os"
clidocstool "github.com/docker/cli-docs-tool"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/commands"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const defaultSourcePath = "docs/reference/commandline/"
type options struct {
source string
target string
}
func gen(opts *options) error {
log.SetFlags(0)
dockerCLI, err := command.NewDockerCli()
if err != nil {
return err
}
cmd := &cobra.Command{
Use: "docker [OPTIONS] COMMAND [ARG...]",
Short: "The base command for the Docker CLI.",
}
commands.AddCommands(cmd, dockerCLI)
c, err := clidocstool.New(clidocstool.Options{
Root: cmd,
SourceDir: opts.source,
TargetDir: opts.target,
Plugin: false,
})
if err != nil {
return err
}
return c.GenYamlTree(cmd)
}
func run() error {
opts := &options{}
flags := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError)
flags.StringVar(&opts.source, "source", defaultSourcePath, "Docs source folder")
flags.StringVar(&opts.target, "target", defaultSourcePath, "Docs target folder")
if err := flags.Parse(os.Args[1:]); err != nil {
return err
}
return gen(opts)
}
func main() {
if err := run(); err != nil {
log.Printf("ERROR: %+v", err)
os.Exit(1)
}
}

13
docs/go.mod Normal file
View File

@ -0,0 +1,13 @@
module github.com/docker/cli/docs
// dummy go.mod to avoid dealing with dependencies specific
// to docs generation and not really part of the project.
go 1.16
//require (
// github.com/docker/cli v0.0.0+incompatible
// github.com/docker/cli-docs-tool v0.3.0
//)
//
//replace github.com/docker/cli v0.0.0+incompatible => ../

7
docs/tools.go Normal file
View File

@ -0,0 +1,7 @@
// +build tools
package main
import (
_ "github.com/docker/cli-docs-tool"
)

View File

@ -1,4 +0,0 @@
FROM scratch
COPY docs /docs
# CMD cannot be nil so we set it to empty string
CMD [""]

View File

@ -1,116 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/commands"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const descriptionSourcePath = "docs/reference/commandline/"
func generateCliYaml(opts *options) error {
dockerCLI, err := command.NewDockerCli()
if err != nil {
return err
}
cmd := &cobra.Command{
Use: "docker [OPTIONS] COMMAND [ARG...]",
Short: "The base command for the Docker CLI.",
}
commands.AddCommands(cmd, dockerCLI)
disableFlagsInUseLine(cmd)
source := filepath.Join(opts.source, descriptionSourcePath)
fmt.Println("Markdown source:", source)
if err := loadLongDescription(cmd, source); err != nil {
return err
}
if err := os.MkdirAll(opts.target, 0755); err != nil {
return err
}
cmd.DisableAutoGenTag = true
return GenYamlTree(cmd, opts.target)
}
func disableFlagsInUseLine(cmd *cobra.Command) {
visitAll(cmd, func(ccmd *cobra.Command) {
// do not add a `[flags]` to the end of the usage line.
ccmd.DisableFlagsInUseLine = true
})
}
// visitAll will traverse all commands from the root.
// This is different from the VisitAll of cobra.Command where only parents
// are checked.
func visitAll(root *cobra.Command, fn func(*cobra.Command)) {
for _, cmd := range root.Commands() {
visitAll(cmd, fn)
}
fn(root)
}
func loadLongDescription(parentCmd *cobra.Command, path string) error {
for _, cmd := range parentCmd.Commands() {
if cmd.HasSubCommands() {
if err := loadLongDescription(cmd, path); err != nil {
return err
}
}
name := cmd.CommandPath()
log.Println("INFO: Generating docs for", name)
if i := strings.Index(name, " "); i >= 0 {
// remove root command / binary name
name = name[i+1:]
}
if name == "" {
continue
}
mdFile := strings.ReplaceAll(name, " ", "_") + ".md"
fullPath := filepath.Join(path, mdFile)
content, err := os.ReadFile(fullPath)
if os.IsNotExist(err) {
log.Printf("WARN: %s does not exist, skipping\n", mdFile)
continue
}
if err != nil {
return err
}
applyDescriptionAndExamples(cmd, string(content))
}
return nil
}
type options struct {
source string
target string
}
func parseArgs() (*options, error) {
opts := &options{}
cwd, _ := os.Getwd()
flags := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError)
flags.StringVar(&opts.source, "root", cwd, "Path to project root")
flags.StringVar(&opts.target, "target", "/tmp", "Target path for generated yaml files")
err := flags.Parse(os.Args[1:])
return opts, err
}
func main() {
opts, err := parseArgs()
if err != nil {
log.Println(err)
}
fmt.Println("Project root: ", opts.source)
fmt.Println("YAML output dir:", opts.target)
if err := generateCliYaml(opts); err != nil {
log.Println("Failed to generate yaml files:", err)
}
}

View File

@ -1,73 +0,0 @@
package main
import (
"regexp"
"strings"
"unicode"
)
var (
// mdHeading matches MarkDown H1..h6 headings. Note that this regex may produce
// false positives for (e.g.) comments in code-blocks (# this is a comment),
// so should not be used as a generic regex for other purposes.
mdHeading = regexp.MustCompile(`^([#]{1,6})\s(.*)$`)
// htmlAnchor matches inline HTML anchors. This is intended to only match anchors
// for our use-case; DO NOT consider using this as a generic regex, or at least
// not before reading https://stackoverflow.com/a/1732454/1811501.
htmlAnchor = regexp.MustCompile(`<a\s+(?:name|id)="?([^"]+)"?\s*></a>\s*`)
)
// getSections returns all H2 sections by title (lowercase)
func getSections(mdString string) map[string]string {
parsedContent := strings.Split("\n"+mdString, "\n## ")
sections := make(map[string]string, len(parsedContent))
for _, s := range parsedContent {
if strings.HasPrefix(s, "#") {
// not a H2 Section
continue
}
parts := strings.SplitN(s, "\n", 2)
if len(parts) == 2 {
sections[strings.ToLower(parts[0])] = parts[1]
}
}
return sections
}
// cleanupMarkDown cleans up the MarkDown passed in mdString for inclusion in
// YAML. It removes trailing whitespace and substitutes tabs for four spaces
// to prevent YAML switching to use "compact" form; ("line1 \nline\t2\n")
// which, although equivalent, is hard to read.
func cleanupMarkDown(mdString string) (md string, anchors []string) {
// remove leading/trailing whitespace, and replace tabs in the whole content
mdString = strings.TrimSpace(mdString)
mdString = strings.ReplaceAll(mdString, "\t", " ")
mdString = strings.ReplaceAll(mdString, "https://docs.docker.com", "")
var id string
// replace trailing whitespace per line, and handle custom anchors
lines := strings.Split(mdString, "\n")
for i := 0; i < len(lines); i++ {
lines[i] = strings.TrimRightFunc(lines[i], unicode.IsSpace)
lines[i], id = convertHTMLAnchor(lines[i])
if id != "" {
anchors = append(anchors, id)
}
}
return strings.Join(lines, "\n"), anchors
}
// convertHTMLAnchor converts inline anchor-tags in headings (<a name=myanchor></a>)
// to an extended-markdown property ({#myanchor}). Extended Markdown properties
// are not supported in GitHub Flavored Markdown, but are supported by Jekyll,
// and lead to cleaner HTML in our docs, and prevents duplicate anchors.
// It returns the converted MarkDown heading and the custom ID (if present)
func convertHTMLAnchor(mdLine string) (md string, customID string) {
if m := mdHeading.FindStringSubmatch(mdLine); len(m) > 0 {
if a := htmlAnchor.FindStringSubmatch(m[2]); len(a) > 0 {
customID = a[1]
mdLine = m[1] + " " + htmlAnchor.ReplaceAllString(m[2], "") + " {#" + customID + "}"
}
}
return mdLine, customID
}

View File

@ -1,132 +0,0 @@
package main
import "testing"
func TestCleanupMarkDown(t *testing.T) {
tests := []struct {
doc, in, expected string
}{
{
doc: "whitespace around sections",
in: `
## Section start
Some lines.
And more lines.
`,
expected: `## Section start
Some lines.
And more lines.`,
},
{
doc: "lines with inline tabs",
in: `## Some Heading
A line with tabs in it.
Tabs should be replaced by spaces`,
expected: `## Some Heading
A line with tabs in it.
Tabs should be replaced by spaces`,
},
{
doc: "lines with trailing spaces",
in: `## Some Heading with spaces
This is a line.
This is an indented line
### Some other heading
Last line.`,
expected: `## Some Heading with spaces
This is a line.
This is an indented line
### Some other heading
Last line.`,
},
{
doc: "lines with trailing tabs",
in: `## Some Heading with tabs
This is a line.
This is an indented line
### Some other heading
Last line.`,
expected: `## Some Heading with tabs
This is a line.
This is an indented line
### Some other heading
Last line.`,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.doc, func(t *testing.T) {
out, _ := cleanupMarkDown(tc.in)
if out != tc.expected {
t.Fatalf("\nexpected:\n%q\nactual:\n%q\n", tc.expected, out)
}
})
}
}
func TestConvertHTMLAnchor(t *testing.T) {
tests := []struct {
in, id, expected string
}{
{
in: `# <a name=heading1></a> Heading 1`,
id: "heading1",
expected: `# Heading 1 {#heading1}`,
},
{
in: `## Heading 2<a name=heading2></a> `,
id: "heading2",
expected: `## Heading 2 {#heading2}`,
},
{
in: `### <a id=heading3></a>Heading 3`,
id: "heading3",
expected: `### Heading 3 {#heading3}`,
},
{
in: `#### <a id="heading4"></a> Heading 4`,
id: "heading4",
expected: `#### Heading 4 {#heading4}`,
},
{
in: `##### <a id="heading5" ></a> Heading 5`,
id: "heading5",
expected: `##### Heading 5 {#heading5}`,
},
{
in: `###### <a id=hello href=foo>hello!</a>Heading 6`,
id: "",
expected: `###### <a id=hello href=foo>hello!</a>Heading 6`,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.in, func(t *testing.T) {
out, id := convertHTMLAnchor(tc.in)
if id != tc.id {
t.Fatalf("expected: %s, actual: %s\n", tc.id, id)
}
if out != tc.expected {
t.Fatalf("\nexpected: %s\nactual: %s\n", tc.expected, out)
}
})
}
}

View File

@ -1,347 +0,0 @@
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
yaml "gopkg.in/yaml.v2"
)
type cmdOption struct {
Option string
Shorthand string `yaml:",omitempty"`
ValueType string `yaml:"value_type,omitempty"`
DefaultValue string `yaml:"default_value,omitempty"`
Description string `yaml:",omitempty"`
DetailsURL string `yaml:"details_url,omitempty"` // DetailsURL contains an anchor-id or link for more information on this flag
Deprecated bool
MinAPIVersion string `yaml:"min_api_version,omitempty"`
Experimental bool
ExperimentalCLI bool
Kubernetes bool
Swarm bool
OSType string `yaml:"os_type,omitempty"`
}
type cmdDoc struct {
Name string `yaml:"command"`
SeeAlso []string `yaml:"parent,omitempty"`
Version string `yaml:"engine_version,omitempty"`
Aliases string `yaml:",omitempty"`
Short string `yaml:",omitempty"`
Long string `yaml:",omitempty"`
Usage string `yaml:",omitempty"`
Pname string `yaml:",omitempty"`
Plink string `yaml:",omitempty"`
Cname []string `yaml:",omitempty"`
Clink []string `yaml:",omitempty"`
Options []cmdOption `yaml:",omitempty"`
InheritedOptions []cmdOption `yaml:"inherited_options,omitempty"`
Example string `yaml:"examples,omitempty"`
Deprecated bool
MinAPIVersion string `yaml:"min_api_version,omitempty"`
Experimental bool
ExperimentalCLI bool
Kubernetes bool
Swarm bool
OSType string `yaml:"os_type,omitempty"`
}
// GenYamlTree creates yaml structured ref files
func GenYamlTree(cmd *cobra.Command, dir string) error {
emptyStr := func(s string) string { return "" }
return GenYamlTreeCustom(cmd, dir, emptyStr)
}
// GenYamlTreeCustom creates yaml structured ref files
func GenYamlTreeCustom(cmd *cobra.Command, dir string, filePrepender func(string) string) error {
for _, c := range cmd.Commands() {
if !c.Runnable() && !c.HasAvailableSubCommands() {
// skip non-runnable commands without subcommands
// but *do* generate YAML for hidden and deprecated commands
// the YAML will have those included as metadata, so that the
// documentation repository can decide whether or not to present them
continue
}
if err := GenYamlTreeCustom(c, dir, filePrepender); err != nil {
return err
}
}
// TODO: conditionally skip the root command (for plugins)
//
// The "root" command used in the generator is just a "stub", and only has a
// list of subcommands, but not (e.g.) global options/flags. We should fix
// that, so that the YAML file for the docker "root" command contains the
// global flags.
//
// If we're using this code to generate YAML docs for a plugin, the root-
// command is even less useful; in that case, the root command represents
// the "docker" command, and is a "dummy" with no flags, and only a single
// subcommand (the plugin's top command). For plugins, we should skip the
// root command altogether, to prevent generating a useless YAML file.
// if !cmd.HasParent() {
// return nil
// }
basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".yaml"
filename := filepath.Join(dir, basename)
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
if _, err := io.WriteString(f, filePrepender(filename)); err != nil {
return err
}
return GenYamlCustom(cmd, f)
}
// GenYamlCustom creates custom yaml output
// nolint: gocyclo
func GenYamlCustom(cmd *cobra.Command, w io.Writer) error {
const (
// shortMaxWidth is the maximum width for the "Short" description before
// we force YAML to use multi-line syntax. The goal is to make the total
// width fit within 80 characters. This value is based on 80 characters
// minus the with of the field, colon, and whitespace ('short: ').
shortMaxWidth = 73
// longMaxWidth is the maximum width for the "Short" description before
// we force YAML to use multi-line syntax. The goal is to make the total
// width fit within 80 characters. This value is based on 80 characters
// minus the with of the field, colon, and whitespace ('long: ').
longMaxWidth = 74
)
cliDoc := cmdDoc{
Name: cmd.CommandPath(),
Aliases: strings.Join(cmd.Aliases, ", "),
Short: forceMultiLine(cmd.Short, shortMaxWidth),
Long: forceMultiLine(cmd.Long, longMaxWidth),
Example: cmd.Example,
Deprecated: len(cmd.Deprecated) > 0,
}
if len(cliDoc.Long) == 0 {
cliDoc.Long = cliDoc.Short
}
if cmd.Runnable() {
cliDoc.Usage = cmd.UseLine()
}
// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
for curr := cmd; curr != nil; curr = curr.Parent() {
if v, ok := curr.Annotations["version"]; ok && cliDoc.MinAPIVersion == "" {
cliDoc.MinAPIVersion = v
}
if _, ok := curr.Annotations["experimental"]; ok && !cliDoc.Experimental {
cliDoc.Experimental = true
}
if _, ok := curr.Annotations["experimentalCLI"]; ok && !cliDoc.ExperimentalCLI {
cliDoc.ExperimentalCLI = true
}
if _, ok := curr.Annotations["kubernetes"]; ok && !cliDoc.Kubernetes {
cliDoc.Kubernetes = true
}
if _, ok := curr.Annotations["swarm"]; ok && !cliDoc.Swarm {
cliDoc.Swarm = true
}
if o, ok := curr.Annotations["ostype"]; ok && cliDoc.OSType == "" {
cliDoc.OSType = o
}
}
anchors := make(map[string]struct{})
if a, ok := cmd.Annotations["anchors"]; ok && a != "" {
for _, anchor := range strings.Split(a, ",") {
anchors[anchor] = struct{}{}
}
}
flags := cmd.NonInheritedFlags()
if flags.HasFlags() {
cliDoc.Options = genFlagResult(flags, anchors)
}
flags = cmd.InheritedFlags()
if flags.HasFlags() {
cliDoc.InheritedOptions = genFlagResult(flags, anchors)
}
if hasSeeAlso(cmd) {
if cmd.HasParent() {
parent := cmd.Parent()
cliDoc.Pname = parent.CommandPath()
cliDoc.Plink = strings.Replace(cliDoc.Pname, " ", "_", -1) + ".yaml"
cmd.VisitParents(func(c *cobra.Command) {
if c.DisableAutoGenTag {
cmd.DisableAutoGenTag = c.DisableAutoGenTag
}
})
}
children := cmd.Commands()
sort.Sort(byName(children))
for _, child := range children {
if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() {
continue
}
cliDoc.Cname = append(cliDoc.Cname, cliDoc.Name+" "+child.Name())
cliDoc.Clink = append(cliDoc.Clink, strings.Replace(cliDoc.Name+"_"+child.Name(), " ", "_", -1)+".yaml")
}
}
final, err := yaml.Marshal(&cliDoc)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if _, err := fmt.Fprintln(w, string(final)); err != nil {
return err
}
return nil
}
func genFlagResult(flags *pflag.FlagSet, anchors map[string]struct{}) []cmdOption {
var (
result []cmdOption
opt cmdOption
)
const (
// shortMaxWidth is the maximum width for the "Short" description before
// we force YAML to use multi-line syntax. The goal is to make the total
// width fit within 80 characters. This value is based on 80 characters
// minus the with of the field, colon, and whitespace (' default_value: ').
defaultValueMaxWidth = 64
// longMaxWidth is the maximum width for the "Short" description before
// we force YAML to use multi-line syntax. The goal is to make the total
// width fit within 80 characters. This value is based on 80 characters
// minus the with of the field, colon, and whitespace (' description: ').
descriptionMaxWidth = 66
)
flags.VisitAll(func(flag *pflag.Flag) {
opt = cmdOption{
Option: flag.Name,
ValueType: flag.Value.Type(),
DefaultValue: forceMultiLine(flag.DefValue, defaultValueMaxWidth),
Description: forceMultiLine(flag.Usage, descriptionMaxWidth),
Deprecated: len(flag.Deprecated) > 0,
}
if v, ok := flag.Annotations["docs.external.url"]; ok && len(v) > 0 {
opt.DetailsURL = strings.TrimPrefix(v[0], "https://docs.docker.com")
} else if _, ok = anchors[flag.Name]; ok {
opt.DetailsURL = "#" + flag.Name
}
// Todo, when we mark a shorthand is deprecated, but specify an empty message.
// The flag.ShorthandDeprecated is empty as the shorthand is deprecated.
// Using len(flag.ShorthandDeprecated) > 0 can't handle this, others are ok.
if !(len(flag.ShorthandDeprecated) > 0) && len(flag.Shorthand) > 0 {
opt.Shorthand = flag.Shorthand
}
if _, ok := flag.Annotations["experimental"]; ok {
opt.Experimental = true
}
if _, ok := flag.Annotations["deprecated"]; ok {
opt.Deprecated = true
}
if v, ok := flag.Annotations["version"]; ok {
opt.MinAPIVersion = v[0]
}
if _, ok := flag.Annotations["experimentalCLI"]; ok {
opt.ExperimentalCLI = true
}
if _, ok := flag.Annotations["kubernetes"]; ok {
opt.Kubernetes = true
}
if _, ok := flag.Annotations["swarm"]; ok {
opt.Swarm = true
}
// Note that the annotation can have multiple ostypes set, however, multiple
// values are currently not used (and unlikely will).
//
// To simplify usage of the os_type property in the YAML, and for consistency
// with the same property for commands, we're only using the first ostype that's set.
if ostypes, ok := flag.Annotations["ostype"]; ok && len(opt.OSType) == 0 && len(ostypes) > 0 {
opt.OSType = ostypes[0]
}
result = append(result, opt)
})
return result
}
// forceMultiLine appends a newline (\n) to strings that are longer than max
// to force the yaml lib to use block notation (https://yaml.org/spec/1.2/spec.html#Block)
// instead of a single-line string with newlines and tabs encoded("string\nline1\nline2").
//
// This makes the generated YAML more readable, and easier to review changes.
// max can be used to customize the width to keep the whole line < 80 chars.
func forceMultiLine(s string, max int) string {
s = strings.TrimSpace(s)
if len(s) > max && !strings.Contains(s, "\n") {
s = s + "\n"
}
return s
}
// Small duplication for cobra utils
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
}
// applyDescriptionAndExamples fills in cmd.Long and cmd.Example with the
// "Description" and "Examples" H2 sections in mdString (if present).
func applyDescriptionAndExamples(cmd *cobra.Command, mdString string) {
sections := getSections(mdString)
var (
anchors []string
md string
)
if sections["description"] != "" {
md, anchors = cleanupMarkDown(sections["description"])
cmd.Long = md
anchors = append(anchors, md)
}
if sections["examples"] != "" {
md, anchors = cleanupMarkDown(sections["examples"])
cmd.Example = md
anchors = append(anchors, md)
}
if len(anchors) > 0 {
if cmd.Annotations == nil {
cmd.Annotations = make(map[string]string)
}
cmd.Annotations["anchors"] = strings.Join(anchors, ",")
}
}
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() }

View File

@ -1,8 +1,34 @@
#!/usr/bin/env bash
# Generate yaml for docker/cli reference docs
set -eu -o pipefail
mkdir -p docs/yaml/gen
set -eu
GO111MODULE=off go build -o build/yaml-docs-generator github.com/docker/cli/docs/yaml
build/yaml-docs-generator --root "$(pwd)" --target "$(pwd)/docs/yaml/gen"
: "${CLI_DOCS_TOOL_VERSION=v0.3.1}"
export GO111MODULE=auto
function clean {
rm -rf "$buildir"
}
buildir=$(mktemp -d -t docker-cli-docsgen.XXXXXXXXXX)
trap clean EXIT
(
set -x
cp -r . "$buildir/"
cd "$buildir"
# init dummy go.mod
./scripts/vendor init
# install cli-docs-tool and copy docs/tools.go in root folder
# to be able to fetch the required depedencies
go mod edit -modfile=vendor.mod -require=github.com/docker/cli-docs-tool@${CLI_DOCS_TOOL_VERSION}
cp docs/tools.go .
# update vendor
./scripts/vendor update
# build docsgen
go build -mod=vendor -modfile=vendor.mod -tags docsgen -o /tmp/docsgen ./docs/generate.go
)
mkdir -p docs/yaml
set -x
/tmp/docsgen --source "$(pwd)/docs/reference/commandline" --target "$(pwd)/docs/yaml"

View File

@ -5,7 +5,7 @@ set -eu
TYP=$1
usage() {
echo "usage: ./scripts/vendor <update|validate|outdated>"
echo "usage: ./scripts/vendor <init|update|validate|outdated>"
exit 1
}
@ -13,12 +13,14 @@ if [ -z "$TYP" ]; then
usage
fi
init() {
# create dummy go.mod, see comment in vendor.mod
cat > go.mod <<EOL
module github.com/docker/cli
go 1.16
EOL
}
update() {
(set -x ; go mod tidy -modfile=vendor.mod; go mod vendor -modfile=vendor.mod)
@ -42,14 +44,20 @@ outdated() {
}
case $TYP in
"init")
init
;;
"update")
init
update
;;
"validate")
init
update
validate
;;
"outdated")
init
outdated
;;
*)