---
title: "Команды и подкоманды: дерево AddCommand"
description: "Cobra-команды и подкоманды: строим дерево через rootCmd и AddCommand, разбираем поля Use, Short, Long, Example и порядок выбора команды при запуске."
course: "Cobra: CLI-приложения на Go"
url: "https://golangmaster.ru/courses/cobra/lessons/03-komandy-podkomandy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

# Команды и подкоманды: дерево AddCommand

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

Cobra-команды собираются в дерево — и это главная причина ставить фреймворк: задача notes add или kubectl get pods решается объявлением маленьких команд и одной строкой AddCommand, а не слоёным switch по os.Args. Разберём rootCmd, поля справки, вложенные уровни и то, как Cobra выбирает команду при запуске.

Ситуация: делаем утилиту notes — личные заметки в терминале. Пользователь должен уметь notes add «текст заметки», notes list, notes done 3, а скоро ещё notes project add и notes project list. На flag это превращается в машинерию: прочитать os.Args[1], switch по нему, внутри каждого кейса ещё один switch, справку печатать в четырёх местах, не забыть ни одну ветку. На Cobra то же самое — четыре маленьких объявления по 10 строк и связки AddCommand. Разница не в объёме даже, а в том, что дерево читается глазами: вот корень, вот его дети, вот листья с Run. Заодно бесплатно появляется справка, которая всегда актуальна, потому что генерируется из тех же объявлений. В этом уроке соберём такое дерево целиком и проследим путь строки notes project add от терминала до нужной функции.

### Cobra-команды: всё есть *cobra.Command

Первая вещь, которую надо принять: в Cobra нет отдельного типа для корня и отдельного для подкоманд. Команда — это всегда значение *cobra.Command: тот же самый struct с полями Use, Short, Long, Run и методом AddCommand. Корень отличается только ролью — это то, чьё имя совпадает с именем исполняемого файла, и к нему привязывают детей. Подкоманда может сама быть родителем — привяжете к ней ребёнка, и дерево вырастет на уровень. Такой дизайн даёт важное следствие: любая функция, принимающая *cobra.Command, работает и с корнем, и с листом — от справки до тестов. Файлы проекта при этом организуются один-к-одному с деревом: root.go держит корень, add.go — команду add, и новый файл подключается к пакету командой cobra-cli add, о механике которой поговорим в конце. Команды-данные плюс поля-функции вроде Run и Args — тот же паттерн, на котором в Go строят http.Handler: поведение объявляется на месте, и читать дерево можно как таблицу.

### root.go: каркас утилиты notes

```go
package cmd

import "github.com/spf13/cobra"

var rootCmd = &cobra.Command{
        Use:   "notes",
        Short: "Заметки в терминале",
        Long:  "Notes — CLI для личных заметок: add, list и done.",
}

func Execute() error {
        return rootCmd.Execute()
}
```

Каркас вы уже видели во втором уроке — здесь он нарочно минимален. Use задаёт имя корня, по которому Cobra распознает вызов; Long пока одна строка, потому что разворачивать нечего. Execute возвращают ошибку вызывающему — в main можно решить, каким будет код выхода; полное управление ошибками — отдельная тема ближе к середине курса.

### Первая подкоманда: notes add

```go
package cmd

import (
        "fmt"

        "github.com/spf13/cobra"
)

var addCmd = &cobra.Command{
        Use:   "add",
        Short: "Добавить новую заметку",
        Long:  "Добавляет заметку в хранилище. Текст передаётся аргументом.",
        Run: func(cmd *cobra.Command, args []string) {
                fmt.Println("добавляю заметку:", args[0])
        },
}

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

addCmd — та же *cobra.Command, что и корень, но уже с Run: именно эта функция выполнится при вызове notes add. Строка rootCmd.AddCommand(addCmd) в init() привязывает команду к дереву — без неё файл бы компилировался, а команда не появлялась ни в справке, ни в маршрутизации. args[0] — первый аргумент после слова add.

### Use, Short, Long, Example: поля, из которых растёт справка

Четыре поля cobra.Command описывают, как команда выглядит в глазах пользователя, и каждое видно в строго определённом месте. Use — самое нагруженное: из него берётся имя для сопоставления с введённой строкой (берётся первое слово), оно же печатается в шапке справки, и в нём же можно показать сигнатуру вызова: Use: "add [текст]" подскажет, что после add ждут аргумент. Short — однострочное описание; именно оно появляется списком в справке родителя, когда пользователь выполняет notes --help. Long — развёрнутый текст, который Cobra печатает в справке самой команды: notes add --help. Example — готовые строки вызова, выводящиеся блоком Examples:; заполняйте его реальными, копируемыми вызовами, а не выдуманными. Есть и другие поля — Aliases для альтернативных имён, Version для корня, — но эти четыре задают 90 процентов впечатления от утилиты. Пишите Short так, чтобы он читался в общем списке, а Long — так, будто объясняете коллеге лично. В собранной справке эти поля превращаются в секции Usage:, Examples: и развёрнутое описание — пользователь привыкнет к вашей утилите за минуты, если секции заполнены.

### Поля cobra.Command и где их видит пользователь

| Поле | Что хранит | Где показывается |
| --- | --- | --- |
| Use | имя команды + подсказка аргументов: add [текст] | маршрутизация, шапка справки |
| Short | одна строка описания | список команд в справке родителя |
| Long | развёрнутое описание, может быть многострочным | справка самой команды --help |
| Example | готовые строки вызова | блок Examples: в справке |
| Aliases | альтернативные имена команды | маршрутизация, справка |
| Version | строка версии | флаг --version у корня |

### Вложенные подкоманды: третий уровень дерева

Заметки обрастут проектами — и справка должна отразить это: notes project add, notes project list. В Cobra вложенность — это просто AddCommand на любом уровне: у project нет своего Run, потому что это групповая команда, контейнер для детей. Общее правило звучит так: у команды, у которой есть дети, Run не пишут — вызов notes project без уточнения напечатает справку со списком детей, и это ровно то, чего ждёт пользователь. Дерево углубляется без изменения правил: четвёртый и пятый уровень технически ничем не отличаются от третьего. Другое дело — стоит ли: в реальных CLI глубже трёх уровней почти не встретишь, потому что пользователю приходится запоминать путь целиком. kubectl и git держатся двух-трёх уровней, и мой совет тот же: если чувствуете, что дерево распухает вглубь, чаще всего команды просятся на разбиение или на флаги.

### notes project add: подкоманда подкоманды

```go
var projectCmd = &cobra.Command{
        Use:   "project",
        Short: "Команды над проектами",
}

var projectAddCmd = &cobra.Command{
        Use:   "add",
        Short: "Создать новый проект",
        Run: func(cmd *cobra.Command, args []string) {
                fmt.Println("создаю проект:", args[0])
        },
}

func init() {
        rootCmd.AddCommand(projectCmd)
        projectCmd.AddCommand(projectAddCmd)
}
```

Два объявления и две связки — весь третий уровень. projectCmd без Run: при вызове notes project Cobra напечатает справку со списком детей. projectAddCmd привязан не к корню, а к projectCmd — отсюда путь notes project add. Имена переменных свободны: маршрутизация смотрит только в поле Use. Поле Hide: true спрячет команду из справки, не отключив её вызов — пригодится для служебных веток дерева.

### Как Cobra находит команду при запуске

Трассируем строку notes project add. Execute() разбивает os.Args и начинает с корня: первым словом ищет совпадение среди детей корня — Use project найден. Дальше то же самое на следующем уровне: среди детей project ищем add — найден. У найденной листовой команды Cobra парсит флаги, проверяет аргументы и вызывает Run. Всё это видно только в настоящем терминале: песочница сайта Cobra не исполняет — у неё нет os.Args, поэтому примеры гоняйте локально с собранной утилитой. Если привыкли читать исходники: за этим стоит execute() и ExecuteC в command.go — файл читается легко, и однажды стоит пробежаться глазами. Три вещи стоит держать в голове. Совпадение идёт по первому слову Use, остальное в Use — подсказки для человека, машина их не матчит. Поиск жадный и останавливается на первом совпадении: два ребёнка с одинаковым Use — ошибка дизайна, сработает тот, кто объявлен первым, и отлаживать это больно. И наконец, всё дерево строится до маршрутизации: init-функции пакетов отрабатывают при импорте, поэтому порядок файлов в проекте не важен, важны только связи AddCommand.

### Неизвестная команда: подсказки вместо молчания

Пользователь опечатался: notes ad. Пустой CLI молча упал бы или напечатал что-то техническое; Cobra перебирает детей корня и находит add — совпадает по префиксу и по расстоянию Левенштейна в два символа. Ответ будет таким: Error: unknown command "ad" for "notes" и блок Did you mean this? add. Это мелочь, от которой зависит первое впечатление: утилита с подсказками выглядит взрослой. Работает она и на третьем уровне — notes project ad намекнёт на add — и выключается через DisableSuggestions, если в вашем проекте подсказки не нужны. Обратная настройка тоже есть: поле SuggestFor: []string{"create"} добавит команде дополнительные якоря для поиска подсказок — пользователь, написавший notes create, получит намёк на add, хотя до совпадения по Левенштейну дело бы не дошло. Похожий механизм подсказывает и флаги: опечатка в незнакомом имени вызовет предложение ближайшего по написанию. Настоящая команда completion, которая ставится в шелл для автодополнения, опирается на то же дерево: оно у вас уже посчитано, и в уроке про автодополнение мы лишь отдадим его шеллу.

> **Команда написана, но не видна в справке.** Классика первого дня: файл add.go лежит в cmd, проект собирается, а notes --help команду не показывает. Почти всегда забыта строка rootCmd.AddCommand(addCmd) в init(). Вторая причина — Use с лишними словами: маршрутизация матчит только первое, а вы искали совпадение по всей строке. Третья — файл в другой папке и не попадает в пакет cmd.

### Проверка: маршрутизация

**Вопрос:** Как Cobra сопоставит строку notes project add с нужной функцией?

- ✅ По дереву: ищет project среди детей корня, затем add среди детей project
- Ищет подстроку project add во всех Use разом
- Передаёт всю строку в Run корня, там разбирают вручную
- Использует порядок объявления файлов в папке cmd

Маршрутизация жадная и послойная: первый шаг — дети корня по первому слову Use, следующий шаг — дети найденной команды. Порядок файлов не важен: init-функции строят дерево до любого запуска.

### Проверка: Short и Long

**Вопрос:** Где пользователь увидит Short-описание команды add?

- Только в справке notes add --help
- ✅ В списке команд при notes --help
- Только в выводе после запуска notes add
- Short виден только разработчику в коде

Short — аннотация в списке команд родителя: notes --help показывает одну строку на каждую подкоманду. В справке самой команды печатается Long, а Short там не нужен. Это разделение — причина, по которой Short держат короче одной строки.

### Что напечатает notes без аргументов

```go
var rootCmd = &cobra.Command{
        Use:   "notes",
        Short: "Заметки в терминале",
}

var addCmd = &cobra.Command{
        Use:   "add",
        Short: "Добавить новую заметку",
        Run: func(cmd *cobra.Command, args []string) {
                fmt.Println("add:", args[0])
        },
}

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

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

- add: пустая строка — Run выполнится с пустым срезом
- Ошибку: команда без Run запрещена
- ✅ Справку: Long корня и список подкоманд с Short-описаниями
- Ничего и код выхода 0

У корня нет Run — Cobra печатает help: имя, Short и список детей с их Short-описаниями. Так работают все групповые команды: kubectl без аргументов тоже показывает справку. Код выхода при этом ненулевой, если Execute вернул ошибку.

### Восстановите команду done

Впишите имя команды, поле для тела команды и метод привязки к корню.

```go
var doneCmd = &cobra.Command{
        Use:   "___",
        Short: "Отметить заметку выполненной",
        ___: func(cmd *cobra.Command, args []string) {
                fmt.Println("done:", args[0])
        },
}

func init() {
        rootCmd.___(doneCmd)
}
```

Подсказка: Имя в Use должно совпадать со словом вызова; тело команды — поле Run; привязка — AddCommand.

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

### Соберите путь вызова из шести строк

Расставьте строки: от объявления команды list через привязку к корню до запуска из main.

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

1. `var listCmd = &cobra.Command{`
2. `  Use:   "list",`
3. `  Short: "Показать все заметки",`
4. `}`
5. `func init() { rootCmd.AddCommand(listCmd) }`
6. `func main() { cmd.Execute() }`

### Карточки: дерево команд

- **rootCmd** — Корень дерева: его Use совпадает с именем исполняемого файла, к нему привязывают подкоманды первого уровня.
- **AddCommand** — Метод привязки ребёнка к родителю: rootCmd.AddCommand(addCmd). Без вызова команда не попадает ни в справку, ни в маршрутизацию.
- **Use** — Поле с именем команды: матчится первое слово. Остаток строки Use — подсказка пользователю: add [текст].
- **Short vs Long** — Short — одна строка в списке родителя; Long — развёрнутый текст в справке самой команды.
- **Групповая команда** — Команда без Run, контейнер для детей: notes project. Вызов без уточнения печатает справку со списком детей.
- **Did you mean this?** — Подсказка Cobra при опечатке: ищет ребёнка по префиксу и расстоянию Левенштейна не больше 2.

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

Дерево стоит — теперь команды надо научить принимать данные. В следующем уроке разберём [аргументы](/courses/cobra/lessons/04-argumenty/) и их валидацию: почему args[0] без проверки — мина, и как заставить Cobra отклонять вызов с не тем количеством значений ещё до Run. Потом — [флаги](/courses/cobra/lessons/05-flogy-localnye/): локальные и персистентные, которые наследуются всем поддереву. Когда список подкоманд вырастет до десятка, пригодится урок про [хуки PreRun](/courses/cobra/lessons/13-hooks-prerun/) — общий код подготовки перед каждой командой. А проверять команды без запуска настоящего терминала научит урок про [тестирование Cobra](/courses/cobra/lessons/20-testirovanie-cobra/).

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

- В Cobra корень и подкоманды — один тип *cobra.Command; иерархию задаёт только AddCommand.
- Use матчится первым словом: остальное в строке Use — подсказки пользователю, не маршрут.
- Short виден в списке родителя, Long — в справке самой команды, Example — блоком примеров.
- Групповая команда без Run при вызове печатает справку со списком детей — Run пишут только листьям.
- Вложенность — тот же AddCommand на любом уровне; глубже трёх уровней в реальных CLI почти не бывает.
- Забытый AddCommand — причина номер один «команда не видна в справке».
- Опечатки Cobra ловит подсказкой Did you mean this? — по префиксу и расстоянию Левенштейна.

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

Разверните утилиту notes из урока до трёх команд: add, list, done — каждая со своим файлом в cmd и собственной парой Use/Short. Затем добавьте уровень project с двумя детьми: notes project add и notes project list, у group-команды Run не пишите. Соберите проект, вызовите notes --help и notes project --help и убедитесь, что справка на обоих уровнях показывает ваши Short-описания, а notes proj подсказывает Did you mean this? project. И последний штрих: повесьте на add алиас Aliases: []string{"n"} и проверьте, что notes n срабатывает так же, как полное имя.

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

### Сколько уровней вложенности поддерживает Cobra?

Технически неограниченно: каждый уровень — это AddCommand над командой-родителем. Практический потолок — три уровня: глубже пользователю тяжело запоминать путь, и обычно часть дерева превращают в флаги.

### Что делать, если подкоманда не видна в справке?

Проверьте три вещи: вызов rootCmd.AddCommand в init(), первое слово Use и принадлежность файла пакету cmd. В девяти случаях из десяти пропущена именно связка AddCommand.

### Нужно ли вызывать Execute для каждой подкоманды?

Нет. Execute вызывается один раз у корня — из main через cmd.Execute(). Cobra сама спустится по дереву до нужного листа и выполнит его Run. Отдельные Execute на подкомандах сломают маршрутизацию.
