Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 3 из 25 ~20 мин +50 XP

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

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

Содержание урока · 10 разделов

Ситуация: делаем утилиту 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, которая ставится в шелл для автодополнения, опирается на то же дерево: оно у вас уже посчитано, и в уроке про автодополнение мы лишь отдадим его шеллу.

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

Как 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

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

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

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

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

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

Термин

rootCmd

нажмите, чтобы перевернуть
Определение

Корень дерева: его Use совпадает с именем исполняемого файла, к нему привязывают подкоманды первого уровня.

1 / 6

Что дальше#

Дерево стоит — теперь команды надо научить принимать данные. В следующем уроке разберём аргументы и их валидацию: почему 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)

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

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

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

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

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

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

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

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Bubble Tea25 уроков~10 чпрактика25 уроков уже доступны

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