Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 29 additions & 18 deletions cmd/docgen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import (
"github.com/spf13/pflag"
)

const outputFile = "docs/cli-reference.md"
const (
outputFile = "docs/cli-reference.md"
initialHeadingLevel = 2
dirPermissions = 0o750
)

func main() {
rootCmd, err := cmd.NewRootCommand()
Expand All @@ -30,7 +34,7 @@ func main() {
rootCmd.DisableAutoGenTag = true

dir := filepath.Dir(outputFile)
if err := os.MkdirAll(dir, 0o755); err != nil {
if err := os.MkdirAll(dir, dirPermissions); err != nil {
fmt.Fprintf(os.Stderr, "Error creating directory %s: %v\n", dir, err)
os.Exit(1)
}
Expand All @@ -40,31 +44,36 @@ func main() {
fmt.Fprintf(os.Stderr, "Error creating file %s: %v\n", outputFile, err)
os.Exit(1)
}
defer f.Close()

fmt.Fprintln(f, "# CLI Reference")
fmt.Fprintln(f)
fmt.Fprintln(f, "<!-- This file is auto-generated from the Cobra command tree. Do not edit manually. -->")
fmt.Fprintln(f)
defer func() {
if cerr := f.Close(); cerr != nil {
fmt.Fprintf(os.Stderr, "Error closing file %s: %v\n", outputFile, cerr)
}
}()

_, _ = fmt.Fprintln(f, "# CLI Reference")
_, _ = fmt.Fprintln(f)
_, _ = fmt.Fprintln(f, "<!-- This file is auto-generated from the Cobra command tree. Do not edit manually. -->")
_, _ = fmt.Fprintln(f)

generateCommandDoc(f, rootCmd, 2)
generateCommandDoc(f, rootCmd, initialHeadingLevel)

fmt.Printf("CLI reference generated at %s\n", outputFile)
}

func generateCommandDoc(f *os.File, command *cobra.Command, headingLevel int) {
heading := strings.Repeat("#", headingLevel)
fmt.Fprintf(f, "%s %s\n\n", heading, command.CommandPath())
_, _ = fmt.Fprintf(f, "%s %s\n\n", heading, command.CommandPath())

if command.Long != "" {
fmt.Fprintf(f, "%s\n\n", command.Long)
_, _ = fmt.Fprintf(f, "%s\n\n", command.Long)
} else if command.Short != "" {
fmt.Fprintf(f, "%s\n\n", command.Short)
_, _ = fmt.Fprintf(f, "%s\n\n", command.Short)
}

if command.UseLine() != "" {
fmt.Fprintf(f, "**Usage:**\n\n")
fmt.Fprintf(f, "```\n%s\n```\n\n", command.UseLine())
_, _ = fmt.Fprintf(f, "**Usage:**\n\n")
_, _ = fmt.Fprintf(f, "```\n%s\n```\n\n", command.UseLine())
}

writeFlags(f, "Flags", command.NonInheritedFlags())
Expand All @@ -74,6 +83,7 @@ func generateCommandDoc(f *os.File, command *cobra.Command, headingLevel int) {
if sub.IsAdditionalHelpTopicCommand() {
continue
}

generateCommandDoc(f, sub, headingLevel+1)
}
}
Expand All @@ -84,6 +94,7 @@ func writeFlags(f *os.File, title string, flags *pflag.FlagSet) {
}

hasVisible := false

flags.VisitAll(func(flag *pflag.Flag) {
if !flag.Hidden {
hasVisible = true
Expand All @@ -94,9 +105,9 @@ func writeFlags(f *os.File, title string, flags *pflag.FlagSet) {
return
}

fmt.Fprintf(f, "**%s:**\n\n", title)
fmt.Fprintln(f, "| Flag | Shorthand | Default | Description |")
fmt.Fprintln(f, "|------|-----------|---------|-------------|")
_, _ = fmt.Fprintf(f, "**%s:**\n\n", title)
_, _ = fmt.Fprintln(f, "| Flag | Shorthand | Default | Description |")
_, _ = fmt.Fprintln(f, "|------|-----------|---------|-------------|")

flags.VisitAll(func(flag *pflag.Flag) {
if flag.Hidden {
Expand All @@ -113,9 +124,9 @@ func writeFlags(f *os.File, title string, flags *pflag.FlagSet) {
defValue = `""`
}

fmt.Fprintf(f, "| --%s | %s | %s | %s |\n",
_, _ = fmt.Fprintf(f, "| --%s | %s | %s | %s |\n",
flag.Name, shorthand, defValue, flag.Usage)
})

fmt.Fprintln(f)
_, _ = fmt.Fprintln(f)
}
21 changes: 18 additions & 3 deletions cmd/docgen/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ func generateCLIReference(t *testing.T, rootCmd *cobra.Command) string {
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
defer tmpFile.Close()

defer func() {
if cerr := tmpFile.Close(); cerr != nil {
t.Logf("warning: failed to close temp file: %v", cerr)
}
}()

generateCommandDoc(tmpFile, rootCmd, 2)

Expand All @@ -57,10 +62,12 @@ func generateCLIReference(t *testing.T, rootCmd *cobra.Command) string {
// collectCommands recursively collects all commands in the Cobra tree.
func collectCommands(command *cobra.Command) []*cobra.Command {
commands := []*cobra.Command{command}

for _, sub := range command.Commands() {
if sub.IsAdditionalHelpTopicCommand() {
continue
}

commands = append(commands, collectCommands(sub)...)
}

Expand Down Expand Up @@ -91,6 +98,7 @@ func TestPropertyCLIReferenceCompleteness(t *testing.T) {
if description == "" {
description = command.Short
}

if description != "" && !strings.Contains(output, description) {
t.Fatalf("description %q for command %q not found in CLI reference output", description, commandPath)
}
Expand All @@ -100,6 +108,7 @@ func TestPropertyCLIReferenceCompleteness(t *testing.T) {
if flag.Hidden {
return
}

if !strings.Contains(output, flag.Name) {
t.Fatalf("flag name %q for command %q not found in CLI reference output", flag.Name, commandPath)
}
Expand All @@ -108,6 +117,7 @@ func TestPropertyCLIReferenceCompleteness(t *testing.T) {
if defValue == "" {
defValue = `""`
}

if !strings.Contains(output, defValue) {
t.Fatalf("flag default value %q for flag %q of command %q not found in CLI reference output", defValue, flag.Name, commandPath)
}
Expand All @@ -122,6 +132,7 @@ func TestPropertyCLIReferenceCompleteness(t *testing.T) {
if flag.Hidden {
return
}

if !strings.Contains(output, flag.Name) {
t.Fatalf("inherited flag name %q for command %q not found in CLI reference output", flag.Name, commandPath)
}
Expand All @@ -130,6 +141,7 @@ func TestPropertyCLIReferenceCompleteness(t *testing.T) {
if defValue == "" {
defValue = `""`
}

if !strings.Contains(output, defValue) {
t.Fatalf("inherited flag default value %q for flag %q of command %q not found in CLI reference output", defValue, flag.Name, commandPath)
}
Expand Down Expand Up @@ -165,6 +177,7 @@ func extractNavPages(nav []map[string]interface{}) []string {
subNav = append(subNav, m)
}
}

pages = append(pages, extractNavPages(subNav)...)
}
}
Expand Down Expand Up @@ -192,6 +205,7 @@ func findProjectRoot(t *testing.T) string {
if parent == dir {
t.Fatal("could not find project root (no mkdocs.yml found)")
}

dir = parent
}
}
Expand All @@ -200,7 +214,7 @@ func findProjectRoot(t *testing.T) string {
func loadNavPages(t *testing.T, projectRoot string) []string {
t.Helper()

data, err := os.ReadFile(filepath.Join(projectRoot, "mkdocs.yml"))
data, err := os.ReadFile(filepath.Join(projectRoot, "mkdocs.yml")) //nolint:gosec // path is constructed from known project root
if err != nil {
t.Fatalf("failed to read mkdocs.yml: %v", err)
}
Expand Down Expand Up @@ -234,7 +248,7 @@ func TestPropertyDocumentationPageStructure(t *testing.T) {
fullPath := filepath.Join(projectRoot, "docs", page)

// Property: the file must exist
content, err := os.ReadFile(fullPath)
content, err := os.ReadFile(fullPath) //nolint:gosec // path is constructed from known project root and nav entries
if err != nil {
t.Fatalf("page %q listed in nav does not exist at %q: %v", page, fullPath, err)
}
Expand All @@ -261,6 +275,7 @@ func TestPropertyDocumentationPageStructure(t *testing.T) {
// Property: after the heading, there must be at least one non-empty paragraph
// of introductory text (skip blank lines between heading and paragraph)
foundParagraph := false

for i := 1; i < len(lines); i++ {
line := strings.TrimSpace(lines[i])

Expand Down
Loading