---
title: "Генерация документации: man и markdown"
description: "cobra generate docs: man-страницы через doc.GenManTree, markdown через GenMarkdownTree, docs-команда в проекте и README-секция из Long."
course: "Cobra: CLI-приложения на Go"
url: "https://golangmaster.ru/courses/cobra/lessons/19-dokumentaciya-cli/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 14
---

# Генерация документации: man и markdown

*Курс «Cobra: CLI-приложения на Go», урок 19 из 25. ~14 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/cobra/lessons/19-dokumentaciya-cli/*

cobra generate docs умеет всё сама: пакет github.com/spf13/cobra/doc превращает дерево команд в man-страницы и markdown-файлы за один вызов, из тех же Long и Short, что вы уже написали для справки. Здесь разберём GenMarkdownTree, GenManTree, служебную docs-команду в проекте и правило хранения сгенерированных файлов.

Флаг --help выручает за терминалом, но не на сайте проекта, не в man-пейджере и не в тексте для новых сотрудников. У всех крупных утилит на Cobra — gh, kubectl, hugo — есть сайт с документацией по каждой команде и man-страницы в дистрибутивных пакетах, и ни одна команда не пишет это руками: тексты справки переливаются в документацию автоматически. Источник один — поля Use, Short, Long и Example, которые вы заполняли с первого урока; из них пакет doc собирает и markdown для сайта, и troff для man. Подход снимает главный страх документации — рассинхрон: справка и страницы обновляются одним релизом, потому что физически один и тот же текст. Сюда же примыкает и README: его раздел про команды собирается из тех же Long, а не из отдельного файла, который умеет устаревать к каждому релизу. В этом уроке добавим к утилите notes служебную команду docs, посмотрим, какие файлы получаются и где их хранить, и обсудим, что делать с README, который живёт рядом с генерацией.

### cobra generate docs: пакет spf13/cobra/doc

Генераторы живут в отдельном модуле github.com/spf13/cobra/doc — он ставится go get и тянет зависимости только для man: markdown-генератору достаточно стандартной библиотеки. Основные функции: doc.GenMarkdownTree(cmd, dir) рекурсивно обходит дерево команд от корня и пишет один markdown-файл на команду, doc.GenManTree(cmd, header, dir) делает то же для man-страниц, есть и yaml-вариант для дата-центричных конвейеров. Имена файлов выводятся из пути команды: для notes list получится notes_list.md, для вложенной notes list done — notes_list_done.md. Скрытые и недоступные команды в обход не попадают: completion и ваши Hidden-хелперы останутся внутренним делом утилиты, что обычно и нужно. Содержимое страницы — Long как вступление, usage с флагами, алиасы, примеры; то есть качество документации на выходе равно качеству полей справки. Плохой Long в исходнике не станет хорошей страницей на сайте.

### Служебная команда docs

```go
var docsCmd = &cobra.Command{
        Use:    "docs",
        Short:  "Генерирует markdown-документацию",
        Hidden: true,
        RunE: func(cmd *cobra.Command, args []string) error {
                return doc.GenMarkdownTree(cmd.Root(), "./docs")
        },
}

func init() {
        rootCmd.AddCommand(docsCmd)
}
```

Команда спрятана: генерация — забота сборочного процесса, а не пользователя. cmd.Root() даёт корень дерева, GenMarkdownTree обходит его целиком и создаёт каталог, если его нет. RunE возвращает ошибку генератора наружу — коды выхода и обработка ошибок работают как у любой команды, а вызов notes docs в Makefile закрывает весь раздел документации.

### Проверка: что попадёт в документацию

**Вопрос:** В проекте есть скрытая команда gen-completion и стандартная команда completion. Что о них напишет doc.GenMarkdownTree?

- Обе попадут в файлы как обычные команды
- ✅ Ни одна: недоступные и скрытые команды пропускаются
- Только completion: gen-completion спрятана
- Обе попадут, но в общий файл index.md

Генератор обходит только доступные команды: для скрытой IsAvailableCommand возвращает false, и ветка пропускается вместе с поддеревом. completion, генерируемая Cobra, тоже скрыта от обхода. Если внутреннюю команду всё же нужно задокументировать — делайте для неё страницу руками или временно снимайте Hidden.

### Какой файл получится

```go
// дерево команд: notes -> list -> done
err := doc.GenMarkdownTree(rootCmd, "./docs")
// имя файла для самой глубокой команды?
```

**Что выведет программа?**

- docs/list_done.md
- ✅ docs/notes_list_done.md
- docs/notes_list_done.txt
- docs/done.md

Имя файла — полный путь команды с пробелами, заменёнными на подчёркивания, плюс расширение .md: notes list done превращается в notes_list_done.md. Корень дерева получит notes.md, промежуточный list — notes_list.md. Из имён файлов строятся и ссылки между страницами документа.

### Man-страницы: GenManTree и section 1

Man-страница — формат troff, который читает команда man; для CLI-утилит это раздел 1, пользовательские программы. Заголовок задаётся структурой doc.GenManHeader: Title — имя утилиты капсом, NOTES, Section — строка раздела, «1». На выходе GenManTree кладёт в каталог файлы вида notes.1 и notes-list.1 — с дефисами, в отличие от подчёркиваний в markdown. Установка — копирование в /usr/share/man/man1/ с сжатием gzip, после чего man notes открывает полноценную страницу с секциями SYNOPSIS и EXAMPLES, собранными из Use и Example. Сгенерированные страницы ставят в пакеты: deb и rpm без man-страницы выглядят небрежно. Внизу каждой страницы и markdown-файла генератор печатает строку об автогенерации с датой; она удобна для аудита, но мешает диффам между релизами, и cmd.DisableAutoGenTag = true у корня отключает её для всего дерева. Секции страницы собираются сами: NAME — из Use, SYNOPSIS — из usage, DESCRIPTION — из Long, EXAMPLES — из Example. Своей секции вставить некуда: если нужен блок «Безопасность» или «Совместимость», он живёт абзацем внутри Long.

### Генерация markdown и man одной командой

```go
var docsCmd = &cobra.Command{
        Use:    "docs",
        Hidden: true,
        RunE: func(cmd *cobra.Command, args []string) error {
                if err := doc.GenMarkdownTree(cmd.Root(), "./docs/md"); err != nil {
                        return fmt.Errorf("markdown: %w", err)
                }
                header := &doc.GenManHeader{Title: "NOTES", Section: "1"}
                if err := doc.GenManTree(cmd.Root(), header, "./docs/man"); err != nil {
                        return fmt.Errorf("man: %w", err)
                }
                return nil
        },
}
```

Обе ошибки оборачиваются через %w с контекстом: в логе сборки будет видно, какой формат упал. Заголовок man вынесен в переменную — Title капсом по конвенции man, Section 1 для пользовательских утилит. Каталоги docs/md и docs/man разнесены, чтобы CI-джоба публиковала их по разным путям без разборки содержимого.

### Где хранить сгенерированные документы

Первое решение — коммитить или нет. Коммит сгенерированных markdown-страниц даёт читабельные диффы и сайт на gh-pages без CI, но каждая правка справки раздувает pull request сотнями строк и порождает конфликты при параллельной работе. Генерация в CI чистит диффы, но требует пайплайна. Разумная середина для CLI: markdown — коммит, man — генерация при сборке пакета. Отдельный вопрос — хостинг: страницы из GenMarkdownTree без подготовки едут на любой генератор сайтов, но выглядят сырыми — без оглавления и перекрёстных ссылок между разделами. README-секцию про команды разумно собирать из тех же Long: небольшой скрипт поверх doc.GenMarkdownTreeCustom с linkHandler расставит ссылки между страницами под конкретный генератор сайта — hugo, mkdocs, docusaurus. Требование у всех схем одно: Long в исходнике — единственный источник текста. Появился второй абзац «того же смысла» в README, и через два релиза они разъезжаются.

### GenMarkdownTreeCustom: своя шапка и ссылки

Обычный генератор пишет файлы без всяких изменений, а сайты обычно ждут служебной шапки: frontmatter с заголовком и датой для hugo, навигационные метаданные для mkdocs. Для этого у пакета есть GenMarkdownTreeCustom — та же генерация с двумя хуками. filePrepender вызывается перед содержимым каждого файла и возвращает строку — сюда кладут frontmatter: имя команды, заголовок, дату релиза. linkHandler получает имя файла ссылки и возвращает URL: под hugo ссылка на notes list переписывается из notes_list.md в /notes/notes_list/, под репозиторий — в относительный путь. Через те же хуки удобно вставлять обложки, бейджи версий и баннеры. Порядок действий простой: сначала гоняют обычный генератор и смотрят на диффы сайта, потом переносят отличия в два хука — и оба остаются в одном файле, который тоже коммитится.

### Docs-команда, Makefile и CI

Скрытая команда docs из блоков выше — не единственное место для генерации; выбирайте по размеру проекта. Малому CLI хватает вызова notes docs из Makefile: цель docs в две строки, разработчик обновляет страницы перед коммитом. Средним проектам удобнее go generate: файл gen.go с директивой рядом с командами, IDE и make invoke одинаково. Крупным — генерация в CI: джоба после сборки вызывает утилиту, артефакты уезжают на сайт и в пакет; локальный запуск не обязателен. Признак зрелой схемы — генерация man и markdown в одном пайплайне с completion-скриптами: все производные артефакты справки обновляются одним тегом релиза. Что точно не стоит делать — держать три места правды: Makefile, CI и скрытую команду с разными путями вывода; рано или поздно они разъедутся, и на сайте появится документация двух версий. Дешёвая страховка в CI: после генерации гоняйте git diff --exit-code по каталогу docs — документация разъехалась со справкой, джоба покраснеет до релиза.

### Чек-лист документации перед релизом

- Long заполнен у всех видимых команд: пустые Long превращаются в пустые страницы сайта.
- Example есть хотя бы у корня и тяжёлых подкоманд — в man он станет секцией примеров.
- notes docs проходит чисто: ошибок генерации нет, файлы перезаписались без конфликтов.
- Скрытые команды не попали в обход — проверьте отсутствие completion и gen-* в docs/.
- Решение по DisableAutoGenTag принято и зафиксировано: с датой или без, но одинаково у всех страниц.
- README не редактировался руками рядом со сгенерированными файлами — правки только через Long.

> **README, переписанный руками.** Классическая авария: документацию сгенерировали, потом вручную отредактировали пару страниц — поправили формулировки, добавили скриншот. Через месяц справка обновилась, notes docs перезаписал страницы, и правки исчезли молча: git увидит только большой дифф, который никто не читал построчно. Правило жёсткое: сгенерированные файлы не редактируются никогда — правится Long у команды и генерация повторяется. Что хочется видеть на странице, а справка не умеет, — выносится в шаблоны через GenMarkdownTreeCustom. Отдельно проверьте cleanup: если генерация переезжает в CI, старые файлы с переименованными командами должны удаляться, иначе сайт вечно показывает мёртвые страницы.

### Проверка: раздел man для CLI

**Вопрос:** В каком разделе man живут пользовательские программы вроде вашей утилиты?

- ✅ Раздел 1
- Раздел 3
- Раздел 5
- Раздел 8

Раздел 1 — исполняемые программы и команды шелла, 3 — библиотечные функции, 5 — форматы файлов, 8 — системное администрирование. Для notes это Section: "1" в GenManHeader, и man notes откроет страницу именно из man1. Страницы в разделе 5 у CLI тоже бывают — для формата конфига, но отдельной командой.

### Соберите вызов GenManTree

Впишите поля заголовка man-страницы.

```go
header := &doc.GenManHeader{
        ___:     "NOTES",
        ___:     "1",
}
err := doc.GenManTree(rootCmd, header, "./docs/man")
```

Подсказка: Первое поле — имя утилиты капсом, второе — номер раздела man для пользовательских программ.

Правильные ответы в пропусках: `Title`, `Section`

### Соберите скрипт генерации man-страниц

Расставьте: заголовок, вызов генератора с обработкой ошибки, вывод ошибки и аварийный выход, финальное сообщение.

Правильный порядок строк:

1. `header := &doc.GenManHeader{Title: "NOTES", Section: "1"}`
2. `if err := doc.GenManTree(rootCmd, header, "./man"); err != nil {`
3. `  fmt.Fprintln(os.Stderr, err)`
4. `  os.Exit(1)`
5. `}`
6. `fmt.Println("man-страницы готовы в ./man")`

### Карточки: документация

- **spf13/cobra/doc** — Отдельный модуль с генераторами документации: GenMarkdownTree, GenManTree, GenYamlTree — обходят дерево команд и пишут по файлу на команду.
- **GenMarkdownTree** — Markdown по всему дереву: Long как вступление, usage, флаги, примеры. Скрытые команды пропускаются; имена файлов — путь команды с подчёркиваниями.
- **GenManHeader** — Заголовок man-страницы: Title капсом, Section "1" для пользовательских утилит. Передаётся в GenManTree; выход — файлы вида notes.1.
- **DisableAutoGenTag** — Флаг команды, отключающий строку об автогенерации с датой внизу страниц — иначе диффы шумят на каждом релизе.

### Что дальше

Документация готова к релизу; осталась защита от регрессий: [тестирование команд CLI](/courses/cobra/lessons/20-testirovanie-cobra/) — следующий урок, где Execute прогоняется в памяти с буферами вместо stdout. Собирает всё в пакет [урок про сборку и goreleaser](/courses/cobra/lessons/23-sborka-versii/): там man-страницы и completion-скрипты ложатся в релизные артефакты, а [структура большого проекта](/courses/cobra/lessons/21-struktura-proekta/) покажет, где в реальном репозитории живёт файл с docs-командой. Обзор курса — на странице [Cobra](/courses/cobra/). Практика: сгенерируйте markdown и man для своей утилиты, откройте man-страницу через man ./docs/man/notes.1 и убедитесь, что Example попал в секцию примеров.

## Коротко: что запомнить

- Пакет spf13/cobra/doc генерирует markdown, man и yaml из тех же Long и Short, что и справка.
- GenMarkdownTree пишет один файл на команду; скрытые и недоступные команды пропускаются.
- Имя markdown-файла — путь команды с подчёркиваниями: notes_list_done.md; man-файлы — с дефисами: notes-list.1.
- GenManHeader задаёт Title капсом и Section "1" для пользовательских утилит.
- Сгенерированные файлы не редактируются руками: правится Long и повторяется генерация.
- Схема хранения: markdown — коммит, man — генерация при сборке пакета; кастомные ссылки — через GenMarkdownTreeCustom.
- README-секция про команды собирается из тех же Long — единственного источника текста.

## Домашнее задание

Добавьте к своей утилите скрытую команду docs с генерацией markdown и man: GenMarkdownTree в docs/md и GenManTree с заголовком Title капсом и Section 1 в docs/man. Прочитайте страницу через man, проверьте, что скрытые команды не попали в обход, и добавьте DisableAutoGenTag, посмотрев, как после этого выглядят диффы между двумя генерациями. И последняя проверка: поменяйте Short одной команды, перегенерируйте и убедитесь, что diff по docs/md показывает ровно одну изменённую страницу.

## Вопросы и ответы по уроку

### Как сгенерировать документацию для Cobra-проекта?

Поставьте модуль github.com/spf13/cobra/doc и вызовите doc.GenMarkdownTree(rootCmd, "./docs") для markdown или doc.GenManTree(rootCmd, header, dir) для man-страниц. Обычно это делает скрытая команда вроде notes docs, которую дергают Makefile и CI. Один файл создаётся на каждую доступную команду, скрытые пропускаются.

### Нужно ли коммитить сгенерированные документы?

Зависит от пайплайна. Коммит markdown даёт читабельные диффы и сайт без CI, но шумит в pull request и конфликтует при параллельной работе; генерация в CI чище, но требует пайплайна. Частая схема: markdown в репозитории, man — при сборке пакета. В любом случае сгенерированные файлы не редактируют руками — правят Long и перегенерируют.

### Почему man-страница не открывается по имени утилиты?

Файл должен лежать в каталоге man-пейджера, например /usr/share/man/man1/notes.1.gz, и иметь правильное расширение раздела. Для локальной проверки сгенерируйте страницу и откройте напрямую: man ./docs/man/notes.1. Не забудьте задать GenManHeader с Title капсом и Section "1" — иначе страница получит дефолтный заголовок.
