ドキュメント自動生成(MD/Man/YAML)と配布

cobra.Command のコマンドツリーには、すべてのサブコマンド階層・概要(Short/Long)・使用例(Example)・フラグの型やデフォルト値が構造化データとして定義されています。そのため、Go のコマンド定義そのものを Single Source of Truth(単一の情報源) として扱い、Web ドキュメントサイトや UNIX の man ページを完全自動生成できます。Cobra 標準の github.com/spf13/cobra/doc サブパッケージと GoReleaser を組み合わせた本番配布パイプラインを解説します。

doc_generation_and_release_1
Single Source of Truth ドキュメント自動生成とリリース成果物パッケージング (cobra/doc & GoReleaser)
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.smartarts import CardList
from drawlib.styles import Styles
from drawlib.text import text

setup(width=144, height=64)

text(
    (72, 59),
    "CLI リファレンス自動生成とリリースパッケージングパイプライン (cobra/doc)",
    style=Styles.DarkBold.patch(text_size=12.0),
)

# Left: Root Command AST Source of Truth
rectangle((28, 30), width=44, height=42, style=Styles.PrimaryNeutral.patch(shape_r=2.0))
phosphor.tree_structure((28, 44), width=6.0, style=Styles.PrimaryBold)
text((28, 36.0), "NewRootCommand()", style=Styles.DarkBold.patch(text_size=11.0))
text((28, 30.5), "DisableAutoGenTag: true", style=Styles.PrimaryBold.patch(text_size=10.0))
text((28, 25.0), "Use / Short / Long / Example", style=Styles.Dark.patch(text_size=9.5))
text((28, 20.0), "Local & Persistent FlagSets", style=Styles.Dark.patch(text_size=9.5))
text((28, 14.5), "cmd/gendocs/main.go", style=Styles.Muted.patch(text_size=9.5))

# Right: 4 Output Formats via CardList
outputs = CardList(
    style=Styles.Neutral,
    text_style=Styles.DarkBold.patch(text_size=10.5),
    description_style=Styles.Dark.patch(text_size=9.5),
    badge_shape="rounded_rect",
    badge_style=Styles.PrimaryNeutral,
    badge_text_style=Styles.PrimaryBold.patch(text_size=10.0),
    direction="vertical",
    spacing=2.0,
)
outputs.add(
    "Markdown ツリー (GenMarkdownTreeCustom)",
    "静的サイト生成器 (Drawlib / Hugo / Docusaurus) 向けリファレンス",
    badge_text="MD",
    style=Styles.PrimaryFlat,
    text_style=Styles.WhiteBold.patch(text_size=10.5),
    description_style=Styles.White.patch(text_size=9.5),
    badge_style=Styles.White,
    badge_text_style=Styles.PrimaryBold.patch(text_size=10.0),
)
outputs.add(
    "ROFF Man ページ (GenManTree)",
    "UNIX man1 マニュアル (mycli-*.1.gz) + SOURCE_DATE_EPOCH",
    badge_text="MAN",
    style=Styles.SecondaryFlat,
    text_style=Styles.WhiteBold.patch(text_size=10.5),
    description_style=Styles.White.patch(text_size=9.5),
    badge_style=Styles.White,
    badge_text_style=Styles.SecondaryBold.patch(text_size=10.0),
)
outputs.add(
    "YAML カタログ (GenYamlTree)",
    "機械可読な CLI スキーマ定義・ガバナンス監査・SDK 連携用",
    badge_text="YML",
    style=Styles.PrimaryNeutral,
)
outputs.add(
    "ReStructuredText (GenReSTTree)",
    "Sphinx / ReadTheDocs エコシステム向けドキュメントツリー",
    badge_text="RST",
    style=Styles.Neutral,
)
outputs.draw(xy=(60, 7), width=78, height=46)

line((50, 30), (60, 30), arrow_head="->", style=Styles.PrimaryBold)

save()

1. cobra/doc の4大ドキュメントジェネレータ

github.com/spf13/cobra/doc パッケージは、cobra.Command ツリーを再帰的に走査し(Hidden: true や Deprecated なコマンドは自動除外)、各サブコマンドごとに1つのリファレンスファイルを出力します。

出力フォーマット 単一コマンド出力(io.Writer) ツリー一括出力(ディレクトリ指定) フロントマター・リンク変換付き出力
Markdown(.md) doc.GenMarkdown(cmd, w) doc.GenMarkdownTree(cmd, dir) doc.GenMarkdownTreeCustom(cmd, dir, prepender, linkHandler)
Man ページ(.1) doc.GenMan(cmd, hdr, w) doc.GenManTree(cmd, hdr, dir) doc.GenManTreeFromOpts(cmd, GenManTreeOptions{...})
YAML(.yaml) doc.GenYaml(cmd, w) doc.GenYamlTree(cmd, dir) doc.GenYamlTreeCustom(cmd, dir, prepender, linkHandler)
ReST(.rst) doc.GenReST(cmd, w) doc.GenReSTTree(cmd, dir) doc.GenReSTTreeCustom(cmd, dir, prepender, linkHandler)

DisableAutoGenTag: true で Git の不要な差分(日付ノイズ)を防ぐ

デフォルト(DisableAutoGenTag: false)では、生成されたすべてのファイルの末尾に実行当日の日付タグが出力されます。

###### Auto generated by spf13/cobra on 10-Nov-2026

このままでは、コマンド仕様を1行も変更していなくても日次 CI のたびに全ドキュメントファイルに Git 差分が発生してしまいます。必ずルートコマンドに rootCmd.DisableAutoGenTag = true を設定してください。

  • cobra/doc はツリー走査時に親コマンドの DisableAutoGenTag: true をすべての子孫コマンドへ自動伝播します(doc/md_docs.go:91-95)。
  • さらに GenManTree では、Reproducible Builds(再現可能ビルド)標準の環境変数 SOURCE_DATE_EPOCH も自動的に認識されます(doc/man_docs.go:127)。

完全なドキュメント生成ツール実装例(cmd/gendocs/main.go)

package main

import (
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/spf13/cobra/doc"
	"example.com/mycli/internal/cli"
)

func main() {
	rootCmd := cli.NewRootCommand()
	// 日付フッタを無効化し、Git 差分を 100% 決定論的(Deterministic)にする
	rootCmd.DisableAutoGenTag = true

	// 1. 静的サイト向けに YAML フロントマター付きで Markdown ツリーを生成
	mdDir := "./dist/docs/markdown"
	_ = os.MkdirAll(mdDir, 0o755)
	filePrepender := func(filename string) string {
		name := filepath.Base(filename)
		base := strings.TrimSuffix(name, filepath.Ext(name))
		title := strings.ReplaceAll(base, "_", " ")
		return fmt.Sprintf("---\ntitle: %q\nslug: %q\n---\n\n", title, base)
	}
	linkHandler := func(name string) string {
		return strings.ToLower(name)
	}
	if err := doc.GenMarkdownTreeCustom(rootCmd, mdDir, filePrepender, linkHandler); err != nil {
		log.Fatalf("markdown generation failed: %v", err)
	}

	// 2. UNIX セクション 1 の Man ページを生成
	manDir := "./dist/docs/man1"
	_ = os.MkdirAll(manDir, 0o755)
	fixedDate := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	header := &doc.GenManHeader{
		Title:   "MYCLI",
		Section: "1",
		Date:    &fixedDate,
		Source:  "MyCLI Enterprise",
		Manual:  "MyCLI User Manuals",
	}
	if err := doc.GenManTree(rootCmd, header, manDir); err != nil {
		log.Fatalf("manpage generation failed: %v", err)
	}

	// 3. YAML スキーマカタログを生成
	yamlDir := "./dist/docs/yaml"
	_ = os.MkdirAll(yamlDir, 0o755)
	if err := doc.GenYamlTree(rootCmd, yamlDir); err != nil {
		log.Fatalf("yaml generation failed: %v", err)
	}
}

2. Go リンカのデッドコード除去(DCE)とバイナリサイズの最適化

CI コンテナや開発者端末に配布される本番 CLI バイナリでは、ファイルサイズと起動速度が重要になります。Go のリンカ(cmd/link)のデッドコード除去(Dead-Code Elimination: DCE)と cobra/doc の関係を理解し、本番バイナリを最小限に保ちましょう。

doc_generation_and_release_2
ビルド時ドキュメント生成ツール (cmd/gendocs) と軽量な本番バイナリ (cmd/mycli) の分離アーキテクチャ
Source Code
from drawlib.canvas import save, setup
from drawlib.lines import line
from drawlib.shapes import rectangle
from drawlib.styles import Styles
from drawlib.text import text

setup(width=144, height=56)

text(
    (72, 51),
    "ビルド時成果物ジェネレータと軽量な本番配布バイナリの分離構成",
    style=Styles.DarkBold.patch(text_size=12.0),
)

# Shared core package
rectangle((28, 25), width=42, height=34, style=Styles.PrimaryNeutral.patch(shape_r=2.0))
text((28, 36.0), "internal/cli", style=Styles.DarkBold.patch(text_size=11.5))
text((28, 30.0), "NewRootCommand()", style=Styles.PrimaryBold.patch(text_size=10.5))
text((28, 24.0), "純粋なコマンドツリー定義", style=Styles.Dark.patch(text_size=10.0))
text((28, 19.0), "cobra/doc に非依存!", style=Styles.Dark.patch(text_size=10.0))
text((28, 13.5), "余分な reflect を排除", style=Styles.Muted.patch(text_size=9.5))

# Top Right: Production Binary
rectangle((102, 36), width=72, height=16, style=Styles.PrimaryFlat.patch(shape_r=2.0))
text((102, 39.5), "cmd/mycli/main.go (本番リリースバイナリ)", style=Styles.WhiteBold.patch(text_size=11.0))
text((102, 33.0), "go build -trimpath -ldflags=\"-s -w\" -> 軽量・高速起動バイナリ", style=Styles.White.patch(text_size=10.0))

# Bottom Right: Build-Time Generator
rectangle((102, 14), width=72, height=18, style=Styles.Neutral.patch(shape_r=2.0))
text((102, 19.0), "cmd/gendocs/main.go (CI / GoReleaser フック)", style=Styles.DarkBold.patch(text_size=11.0))
text((102, 13.5), "ビルド時のみ cobra/doc + go-md2man + yaml.v3 をリンク", style=Styles.Dark.patch(text_size=10.0))
text((102, 8.5), "man1/*.1.gz・completions/*・docs/*.md を一括生成", style=Styles.PrimaryBold.patch(text_size=10.0))

line((49, 31), (66, 36), arrow_head="->", style=Styles.PrimaryBold)
line((49, 19), (66, 14), arrow_head="->", style=Styles.PrimaryBold)

save()

本番バイナリを軽量に保つ3つの鉄則

  1. cobra/doc は cmd/gendocs/main.go に分離する:
    • github.com/spf13/cobra/doc は内部で go-md2man/v2、blackfriday/v2、yaml.v3 をインポートします。本番の cmd/mycli バイナリからこれらを切り離し、ビルド時専用の cmd/gendocs/main.go から NewRootCommand() を呼び出す構成にすれば、配布バイナリに Markdown パーサーや YAML ライブラリが含まれません。
  2. text/template と reflect.Value.MethodByName が DCE に与える影響:
    • Go のリンカ(cmd/link)は到達不能な関数やメソッドをバイナリから除去しますが、プログラム内で reflect.Value.MethodByName(text/template や html/template が内部で使用)が呼ばれると、どの構造体のメソッドが文字列名で動的呼び出しされるか静的に特定できないため、到達可能なすべての構造体のエクスポート済みメソッドがバイナリに残ってしまいます。
    • 独自のテンプレート処理を追加する際は、巨大 SDK クライアントを同じパッケージに抱え込まないよう分離するか、fmt.Fprintf ベースの出力関数(SetHelpFunc)を活用してください。
  3. リリースビルドでのシンボル・DWARF 除去:
    • CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" を指定することで、シンボルテーブル(-s)と DWARF デバッグ情報(-w)を除去し、バイナリサイズを大幅に削減できます。

3. GoReleaser による補完スクリプトと Man ページの同梱配布

brew install mycli や apt install mycli を実行した直後から <Tab> 補完と man mycli が使えるようにするには、cmd/gendocs/main.go で4種類のシェル補完スクリプトも同時出力し、.goreleaser.yaml でパッケージに同梱します。

// cmd/gendocs/main.go 内で4シェルの補完スクリプトを自動生成:
_ = rootCmd.GenBashCompletionV2(bashFile, true)
_ = rootCmd.GenZshCompletion(zshFile)
_ = rootCmd.GenFishCompletion(fishFile, true)
_ = rootCmd.GenPowerShellCompletionWithDesc(pwshFile)

生成した成果物を .goreleaser.yaml でアーカイブおよび Homebrew Formula にマッピングします。

version: 2
before:
  hooks:
    - go run ./cmd/gendocs
    - sh -c "gzip -9 -f ./dist/docs/man1/*.1"

builds:
  - id: mycli
    main: ./cmd/mycli
    env:
      - CGO_ENABLED=0
    flags:
      - -trimpath
    ldflags:
      - -s -w -X example.com/mycli/internal/cli.version={{.Version}}

archives:
  - id: default
    files:
      - LICENSE*
      - README*
      - dist/docs/man1/*.1.gz
      - dist/completions/*

brews:
  - name: mycli
    install: |
      bin.install "mycli"
      man1.install Dir["dist/docs/man1/*.1.gz"]
      bash_completion.install "dist/completions/mycli.bash" => "mycli"
      zsh_completion.install "dist/completions/mycli.zsh" => "_mycli"
      fish_completion.install "dist/completions/mycli.fish"

関連ガイド