Содержание урока · 10 разделов
- Cobra-команды: всё есть *cobra.Command
- Use, Short, Long, Example: поля, из которых растёт справка
- Поля cobra.Command и где их видит пользователь
- Вложенные подкоманды: третий уровень дерева
- Как 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
GOpackage 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
GOpackage 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: подкоманда подкоманды
GOvar 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 с нужной функцией?
Проверка: Short и Long
Где пользователь увидит Short-описание команды add?
Что напечатает notes без аргументов
Прочитайте код и выберите, что он выведет:
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)
}Восстановите команду done
Впишите имя команды, поле для тела команды и метод привязки к корню.
Соберите путь вызова из шести строк
Расставьте строки: от объявления команды list через привязку к корню до запуска из main.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: дерево команд
Что дальше#
Дерево стоит — теперь команды надо научить принимать данные. В следующем уроке разберём аргументы и их валидацию: почему args[0] без проверки — мина, и как заставить Cobra отклонять вызов с не тем количеством значений ещё до Run. Потом — флаги: локальные и персистентные, которые наследуются всем поддереву. Когда список подкоманд вырастет до десятка, пригодится урок про хуки PreRun — общий код подготовки перед каждой командой. А проверять команды без запуска настоящего терминала научит урок про тестирование Cobra.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- В 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 на подкомандах сломают маршрутизацию.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Bubble Tea: TUI-приложения в терминале
Интерактивный курс Bubble Tea — TUI-фреймворка на Go: Elm-архитектура, bubbles, Lip Gloss, мышь и клавиатура. 25 уроков с примерами и итоговым приложением.
- Понимать Elm-архитектуру: Model, Update, View, Init
- Обрабатывать клавиатуру и мышь в терминале
- Писать асинхронные операции через tea.Cmd, Batch и Sequence
- Использовать bubbles: list, textinput, textarea, table, viewport, progress, spinner