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

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

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

Урок 15 из 25 ~18 мин +50 XP

Интерактивный ввод: вопросы и подтверждения

Интерактивный CLI на Go строится на трёх примитивах: bufio.Scanner для вопросов с клавиатуры, подтверждение y/N перед разрушительными действиями и пароль без эха через пакет term. Разберём, когда вопросы уместны, как не зависнуть в CI и почему у каждой интерактивной команды обязан быть флаг --yes.

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

notes reset без вопросов — утилита, которую страшно запускать: одна команда стирает хранилище целиком, и случайная стрелка вверх в истории шелла превращается в потерянные заметки. Интерактивный ввод лечит это: перед разрушительным действием утилита задаёт вопрос и ждёт ответа. Но медаль двусторонняя: тот же вопрос, вставленный в скрипт или CI, либо зависнет навсегда, либо прочтёт мусор из пустого stdin. Поэтому тема урока — не «как спросить», а пара: как спросить у человека и как дать скрипту способ пройти мимо вопроса. Всё это живёт внутри команды Cobra естественным образом: ввод читается в RunE, вопрос печатается в тот же поток, что и остальной вывод, а флаг --yes открывает непрерывный путь для автоматизации. Соберём набор помощников, который потом переедет в итоговый проект курса.

Интерактивный ввод в CLI: bufio.Scanner поверх stdin#

Стандартный способ прочитать строку — bufio.Scanner поверх os.Stdin: scanner := bufio.NewScanner(os.Stdin), затем scanner.Scan() ждёт строку, а scanner.Text() отдаёт её без перевода строки. В контексте Cobra это функция-помощник, которую зовут из RunE: напечатать вопрос через cmd.OutOrStdout(), считать ответ, вернуть строку. Сканер создаётся один на поток ввода: повторные Scan по одному сканеру читают следующие строки, а два сканера на одном stdin начинают делить буфер и терять ввод — заведите один глобальный или передавайте его с параметрами. У сканера есть лимит длины строки — 64 КБ по умолчанию, для ответов на вопросы этого хватает с запасом. Основы Scan мы разбирали в уроке про ввод с клавиатуры базового курса; здесь посмотрим только специфику CLI — что происходит, когда stdin не терминал, и как читать то, что печатать не принято.

Помощник ask: вопрос и одна строка

GO
func ask(cmd *cobra.Command, prompt string) string {
        fmt.Fprintf(cmd.OutOrStdout(), "%s: ", prompt)
        scanner := bufio.NewScanner(os.Stdin)
        if !scanner.Scan() {
                return "" // EOF или ошибка ввода
        }
        return strings.TrimSpace(scanner.Text())
}

Помощник печатает подсказку в OutOrStdout — как и справка, вопрос уважает перенаправление вывода — и читает одну строку из stdin. Scan возвращает false на конце ввода: скрипт с закрытым stdin получит пустой ответ вместо зависания. TrimSpace спасает от случайных пробелов и перевода строк Windows-стиля.

Подтверждение y/N: дефолт против действий#

Подтверждение — не просто вопрос, а вопрос с безопасным дефолтом. Конвенция Unix читается по регистру букв в приглашении: [y/N] с заглавной N означает «пустой ввод — нет», пользователь нажал Enter случайно, и ничего не случилось. Обратный вариант [Y/n] с дефолтом «да» оставьте для действий, которые почти всегда нужны, — но разрушительные действия к ним не относятся никогда. Разбор ответа держите лояльным: Y, y и yes принимаются, всё остальное — отказ, без переспрашиваний и циклов до ввода корректного значения. Показывайте в вопросе масштаб: не «удалить заметки?», а «удалить 142 заметки из ~/notes? отменить это нельзя» — цена нажатия y должна быть видна до нажатия. И печатайте итог после ответа: «удалено 142 заметки» — подтверждение без отчёта выглядит как ложь, потому что пользователь не знает, что именно произошло после его y. Нюанс на продакшене: приглашение из ask уезжает в OutOrStdout, и при перенаправленном stdout оно окажется в файле вместо экрана — пользователь увидит висящую утилиту без вопроса. Если команда рассчитана на оба режима, вопрос печатайте в cmd.ErrOrStderr(): stderr в терминал приходит всегда.

Помощник confirm и команда reset

GO
func confirm(cmd *cobra.Command, prompt string) bool {
        fmt.Fprintf(cmd.OutOrStdout(), "%s [y/N] ", prompt)
        scanner := bufio.NewScanner(os.Stdin)
        if !scanner.Scan() {
                return false // пустой stdin: отказ
        }
        answer := strings.ToLower(strings.TrimSpace(scanner.Text()))
        return answer == "y" || answer == "yes"
}

func runReset(cmd *cobra.Command, args []string) error {
        n, err := countNotes()
        if err != nil {
                return fmt.Errorf("count notes: %w", err)
        }
        if !confirm(cmd, fmt.Sprintf("Удалить %d заметок?", n)) {
                return errDeclined
        }
        return clearStorage()
}

confirm читает одну строку и принимает только y и yes в любом регистре; пустой ввод и всё остальное — отказ. В вопрос подставляется посчитанное число заметок, а не абстрактное «всё». Отказ возвращается как ошибка errDeclined: команда не выполнилась, и код выхода это покажет.

Проверка: случайный Enter

Пользователь запустил notes reset, увидел «Удалить 142 заметок? [y/N]» и нажал Enter. Что произойдёт?

Что вернёт confirm на « Y »

Прочитайте код и выберите, что он выведет:

// stdin содержит строку "  Y  "
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
answer := strings.ToLower(strings.TrimSpace(scanner.Text()))
fmt.Println(answer == "y")

Выбор из вариантов: нумерованное меню#

Когда ответ — не да или нет, а один из вариантов, работает нумерованное меню: печатаете пронумерованный список, читаете число, проверяете диапазон. Формат устойчивее свободного ввода: пользователь не опечатается в названии, а вам не придётся угадывать синонимы. Дефолтный вариант нужен и здесь: пустой ввод выбирает первый пункт или самый безопасный. Для типов заметок, профилей, целей деплоя меню из трёх-семи пунктов читается лучше, чем флаг с перечислением значений в справке; больше семи — время пересматривать интерфейс команды, а не удлинять меню на экран. Разбор строки в число — strconv.Atoi с проверкой ошибки: буква вместо цифры не должна валить утилиту, это обычный отказ, равнозначный нажатию Ctrl+C на вопросе да или нет. В скриптах меню проходят флагом вместо ввода: добавьте к команде --kind и выбирайте вариант без вопросов, а choose оставьте для живого терминала — тот же принцип, что у --yes ниже.

Помощник choose: меню с дефолтом

GO
func choose(cmd *cobra.Command, prompt string, options []string) int {
        fmt.Fprintln(cmd.OutOrStdout(), prompt)
        for i, opt := range options {
                fmt.Fprintf(cmd.OutOrStdout(), "  %d) %s
", i+1, opt)
        }
        fmt.Fprint(cmd.OutOrStdout(), "Номер [1]: ")
        line := ask(cmd, "")
        if line == "" {
                return 0 // дефолт — первый пункт
        }
        n, err := strconv.Atoi(line)
        if err != nil || n < 1 || n > len(options) {
                return -1 // отказ
        }
        return n - 1
}

Меню печатается с единицы: люди считают с единицы, а не с нуля. Пустой ввод выбирает дефолт, буква или число вне диапазона дают отказ вместо паники — Atoi возвращает ошибку, диапазон проверяется явно. Возврат -1 разберите в RunE как отказ пользователя и верните ошибку. Ноль и минус единица — разные значения: дефолт — это выбор, отказ — ошибка, и смешивать их нельзя.

Пароль без эха: пакет term#

Пароль, набранный в открытом терминале, видно соседям по переговорке, программе записи экрана и истории scrollback. Стандартная библиотека эхо не отключает — это делает пакет golang.org/x/term из официального модуля x: term.ReadPassword(int(os.Stdin.Fd())) читает строку с выключенным эхом и возвращает срез байт. Подсказку «Пароль:» печатайте до чтения — ReadPassword сам ничего не выводит. И после чтения поставьте fmt.Println() руками: ReadPassword перевод строки не добавляет, и следующий вывод приклеится к приглашению. Модуль x/term ставится отдельным go get и не тянет зависимостей — для CLI-утилиты это нормальная цена за базовую гигиену ввода. В CI-сценарии пароль всё равно придёт из переменной окружения, а не из клавиатуры, — о пути для скриптов поговорим ниже.

ReadPassword: ввод без отображения

GO
import "golang.org/x/term"

func askPassword(cmd *cobra.Command) (string, error) {
        fmt.Fprint(cmd.OutOrStdout(), "Пароль: ")
        pass, err := term.ReadPassword(int(os.Stdin.Fd()))
        fmt.Println() // ReadPassword не печатает перевод строки
        if err != nil {
                return "", fmt.Errorf("read password: %w", err)
        }
        return string(pass), nil
}

ReadPassword получает дескриптор stdin через os.Stdin.Fd() и возвращает байты без эха: символы не появляются на экране. Подсказка печатается отдельно, перевод строки — руками после чтения. Ошибка возможна, когда stdin не терминал, — на этот случай в следующем блоке есть сценарий.

Флаг --yes: путь для скриптов#

Стандартное решение — флаг --yes: cmd.Flags().BoolP("yes", "y", false, "не спрашивать подтверждение"). В RunE порядок строгий: если --yes не передан и stdin не терминал — откажитесь сразу с подсказкой, как обойти вопрос; если --yes не передан, а терминал есть — задайте вопрос; если --yes передан — выполняйте без вопросов. Отдельная честность: --yes «на всё» — компромисс с риском, и для самого разрушительного уровня утилиты вводят второй барьер — отдельный флаг --force или обязательное длинное значение вроде --yes-i-want-to-delete-142-notes. Готовые библиотеки вроде huh или survey дают красивые виджеты выбора и маскированный ввод — взгляните, когда помошников станет мало, но три функции из этого урока покрывают 90% потребностей CLI без единой зависимости сверх x/term.

Команда с --yes и проверкой терминала

GO
var resetCmd = &cobra.Command{
        Use: "reset",
        RunE: func(cmd *cobra.Command, args []string) error {
                yes, _ := cmd.Flags().GetBool("yes")
                if !yes && !term.IsTerminal(os.Stdin.Fd()) {
                        return fmt.Errorf("stdin не терминал: передайте --yes")
                }
                if !yes && !confirm(cmd, "Удалить все заметки?") {
                        return errDeclined
                }
                return clearStorage()
        },
}

func init() {
        resetCmd.Flags().BoolP("yes", "y", false, "не спрашивать подтверждение")
}

Три ветки: без --yes и без терминала — отказ с подсказкой; без --yes с терминалом — вопрос; с --yes — прямое выполнение. И отказ пользователя, и отказ автоматизации возвращают ошибку: код выхода отличит отмену от работы. IsTerminal проверяет stdin до первого вопроса — CI не зависнет.

Проверка: запуск в CI

notes reset без --yes запущен в CI со связанным пустым stdin. Что сделает утилита из этого урока?

Соберите чтение строки и пароля

Впишите пакет сканера, переменную ошибки и метод дескриптора.

scanner := .NewScanner(os.Stdin) pass, := term.ReadPassword(int(os.Stdin.))

Соберите runReset из шести строк

Расставьте: начало функции, чтение флага, вопрос при отсутствии --yes, отказ, закрытие if, само действие.

Строки

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

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

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

Термин

bufio.Scanner

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

Читатель строк из stdin: Scan ждёт строку, Text отдаёт без перевода строки. Один сканер на поток ввода; EOF — Scan вернёт false.

1 / 6

Что дальше#

Ввод — половина диалога с терминалом; вторая половина — вывод: в следующем уроке про цвета и таблицы добавим к вопросам ANSI-цвета, fatih/color и правило NO_COLOR. Собирается всё вместе в итоговом проекте: там confirm стоит перед очисткой хранилища, а --yes открывает путь скриптам. Когда во время длинной операции пользователь жмёт Ctrl+C вместо ответа, процесс умирает вместе с вопросом — как завершаться корректно, разбирает урок про сигналы и контексты; обзор курса — на странице Cobra. Домашняя работа на закрепление простая: добавьте подтверждение к самой разрушительной команде своей утилиты и не забудьте про флаг --yes — скрипты скажут спасибо.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Ввод в Cobra читается в RunE: bufio.Scanner поверх os.Stdin, вопрос — в cmd.OutOrStdout().
  • Подтверждение [y/N] при пустом вводе означает отказ: случайный Enter не запускает разрушительное действие.
  • Меню выбора — нумерованный список с дефолтом и проверкой диапазона через strconv.Atoi.
  • Пароль — term.ReadPassword с os.Stdin.Fd(): эхо выключено, перевод строки после чтения ставится руками.
  • У любой интерактивной команды есть путь для скриптов: флаг --yes и проверка term.IsTerminal до первого вопроса.
  • EOF в скрипте — не зависание: confirm на пустом stdin отказывает, команда возвращает ошибку.
  • Показывайте в вопросе масштаб действия: 142 заметки, а не абстрактные «все данные».

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

Добавьте в свою утилиту команду очистки с подтверждением: посчитайте записи, покажите их число в вопросе и откажитесь при пустом вводе. Затем повесьте флаг --yes и проверку term.IsTerminal: в терминале команда спрашивает, в скрипте с --yes — молчит, без --yes и без терминала — возвращает ошибку с подсказкой. Проверьте все три сценария локально и с перенаправленным stdin. Затем прогоните сценарий скрипта: echo y | ./notes reset без --yes должен отказать по IsTerminal, а с --yes — пройти молча.

Личные заметкиПусто
Хранятся только в вашем браузере (Cobra · Интерактивный ввод: вопросы и подтверждения)

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

Как прочитать ввод пользователя в команде Cobra?

bufio.NewScanner(os.Stdin) и scanner.Scan() внутри RunE: Scan ждёт строку, Text() возвращает её без перевода строки. Пустой stdin даёт false от Scan — обрабатывайте EOF как отказ, а не как согласие, и заведите один сканер на весь ввод.

Почему команда с вопросом зависает в CI?

Утилита ждёт строку из stdin, а в CI он пуст или закрыт. Оберните вопрос проверкой term.IsTerminal(os.Stdin.Fd()) и добавьте флаг --yes: терминал получает вопрос, скрипты проходят без ввода, а без флага утилита отказывается сразу с подсказкой.

Как сделать ввод пароля без отображения символов?

Пакет golang.org/x/term: term.ReadPassword(int(os.Stdin.Fd())) читает строку с выключенным эхом. Напечатайте подсказку до вызова и fmt.Println() после — ReadPassword сам перевод строки не добавляет, и следующий вывод прилипнет к приглашению.

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

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

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

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

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

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