Содержание

Создание CLI-инструментов с флагами в Go

Что такое хороший CLI

CLI (Command Line Interface) — программа, которая запускается из терминала и принимает параметры через аргументы командной строки.

Хороший CLI-инструмент:

  • Показывает справку при --help
  • Принимает параметры через флаги (--output file.txt, -v)
  • Выводит понятные сообщения об ошибках
  • Возвращает правильные exit-коды (0 = успех, не-0 = ошибка)

Примеры хороших CLI: git, docker, curl, go — все они следуют этим принципам.

Пакет flag — встроенное решение

Go имеет встроенный пакет flag для парсинга аргументов командной строки. Он покрывает большинство задач без внешних зависимостей.

Базовый пример

package main import ( "flag" "fmt" "os" ) func main() { // Определяем флаги outputFile := flag.String("output", "", "Файл для записи результата") verbose := flag.Bool("verbose", false, "Подробный вывод") // Парсим аргументы командной строки flag.Parse() // Получаем оставшиеся аргументы (не флаги) args := flag.Args() if len(args) == 0 { fmt.Fprintln(os.Stderr, "Ошибка: укажите входной файл") os.Exit(1) } inputFile := args[0] if *verbose { fmt.Printf("Обработка файла: %s\n", inputFile) if *outputFile != "" { fmt.Printf("Запись в: %s\n", *outputFile) } } // ... основная логика }

Запуск:

./myapp --verbose --output=result.txt input.txt ./myapp -verbose -output result.txt input.txt # тоже работает ./myapp input.txt # без флагов

Типы флагов

Основные типы

// Строка name := flag.String("name", "default", "Описание флага") // Целое число port := flag.Int("port", 8080, "Порт сервера") count := flag.Int64("count", 0, "Количество итераций") // Булево значение verbose := flag.Bool("verbose", false, "Подробный вывод") // Вызов: --verbose или --verbose=true или --verbose=false // Дробное число rate := flag.Float64("rate", 1.0, "Множитель скорости") // Длительность (time.Duration) timeout := flag.Duration("timeout", 30*time.Second, "Таймаут операции") // Вызов: --timeout=5s, --timeout=2m30s, --timeout=1h

Флаги с привязкой к переменной

Альтернативный синтаксис — привязка флага к существующей переменной:

var verbose bool var port int var name string func init() { flag.BoolVar(&verbose, "verbose", false, "Подробный вывод") flag.IntVar(&port, "port", 8080, "Порт сервера") flag.StringVar(&name, "name", "", "Имя пользователя") } func main() { flag.Parse() // Используем verbose, port, name напрямую (без *) if verbose { fmt.Println("Режим отладки включён") } }

Короткие и длинные флаги

Пакет flag не различает короткие (-v) и длинные (--verbose) флаги автоматически. Но можно создать оба, указывающие на одну переменную:

var verbose bool func init() { flag.BoolVar(&verbose, "v", false, "Подробный вывод") flag.BoolVar(&verbose, "verbose", false, "Подробный вывод") }

Теперь работают все варианты:

  • -v
  • --v
  • -verbose
  • --verbose

Настройка --help

Пакет flag автоматически обрабатывает -h и --help — вызывает flag.Usage() и завершает программу с кодом 2.

По умолчанию flag.Usage выводит список флагов. Чтобы добавить описание программы, переопределите эту функцию:

flag.Usage = func() { fmt.Fprintf(os.Stderr, `mytool — утилита для обработки файлов Использование: mytool [флаги] <входной_файл> Примеры: mytool data.txt Обработать и вывести в консоль mytool -o result.txt data.txt Записать результат в файл mytool -v data.txt Подробный вывод Флаги: `) flag.PrintDefaults() }

Вывод ./mytool --help:

mytool — утилита для обработки файлов

Использование:
  mytool [флаги] <входной_файл>

Примеры:
  mytool data.txt                 Обработать и вывести в консоль
  mytool -o result.txt data.txt   Записать результат в файл
  mytool -v data.txt              Подробный вывод

Флаги:
  -o string
        Файл для записи (по умолчанию stdout)
  -v    Подробный вывод

Exit-коды

CLI-инструменты должны возвращать правильные коды завершения. Это важно для скриптов и автоматизации.

КодЗначение
0Успех
1Общая ошибка (файл не найден, ошибка обработки)
2Неправильное использование (некорректные аргументы)
os.Exit(0) // Всё хорошо os.Exit(1) // Что-то пошло не так os.Exit(2) // Пользователь неправильно вызвал программу

Проверка в bash-скрипте:

if ./myapp input.txt > output.txt; then echo "Обработка успешна" else echo "Ошибка обработки (код: $?)" fi

Вывод ошибок

Ошибки нужно выводить в stderr, а не в stdout. Это позволяет отделить результат работы от сообщений об ошибках:

// Правильно: ошибки в stderr fmt.Fprintln(os.Stderr, "Ошибка: файл не найден") // Неправильно: ошибки в stdout (смешаются с результатом) fmt.Println("Ошибка: файл не найден")

Почему это важно:

# stdout перенаправляется в файл, stderr — в консоль ./myapp input.txt > output.txt # Ошибки покажутся в консоли, а не запишутся в output.txt

Полный пример CLI-приложения

package main import ( "flag" "fmt" "os" ) var version = "dev" func main() { // Флаги var ( outputFile string showVersion bool verbose bool ) flag.StringVar(&outputFile, "o", "", "Файл для записи (по умолчанию stdout)") flag.StringVar(&outputFile, "output", "", "Файл для записи (по умолчанию stdout)") flag.BoolVar(&showVersion, "version", false, "Показать версию") flag.BoolVar(&verbose, "v", false, "Подробный вывод") flag.BoolVar(&verbose, "verbose", false, "Подробный вывод") flag.Usage = func() { fmt.Fprintf(os.Stderr, "mytool — утилита для обработки текстовых файлов\n\n") fmt.Fprintf(os.Stderr, "Использование:\n") fmt.Fprintf(os.Stderr, " mytool [флаги] <файл>\n\n") fmt.Fprintf(os.Stderr, "Примеры:\n") fmt.Fprintf(os.Stderr, " mytool data.txt Вывести в консоль\n") fmt.Fprintf(os.Stderr, " mytool -o out.txt data.txt Записать в файл\n") fmt.Fprintf(os.Stderr, " mytool -v data.txt Подробный вывод\n\n") fmt.Fprintf(os.Stderr, "Флаги:\n") flag.PrintDefaults() } flag.Parse() // --version if showVersion { fmt.Printf("mytool version %s\n", version) os.Exit(0) } // Проверяем аргументы args := flag.Args() if len(args) == 0 { fmt.Fprintln(os.Stderr, "Ошибка: укажите входной файл") fmt.Fprintln(os.Stderr, "Используйте mytool --help для справки") os.Exit(2) } inputFile := args[0] // Проверяем, что файл существует if _, err := os.Stat(inputFile); os.IsNotExist(err) { fmt.Fprintf(os.Stderr, "Ошибка: файл %q не найден\n", inputFile) os.Exit(1) } if verbose { fmt.Fprintf(os.Stderr, "Обработка: %s\n", inputFile) } // Основная логика result, err := processFile(inputFile) if err != nil { fmt.Fprintf(os.Stderr, "Ошибка обработки: %v\n", err) os.Exit(1) } // Вывод результата if outputFile != "" { if err := os.WriteFile(outputFile, []byte(result), 0644); err != nil { fmt.Fprintf(os.Stderr, "Ошибка записи: %v\n", err) os.Exit(1) } if verbose { fmt.Fprintf(os.Stderr, "Записано в: %s\n", outputFile) } } else { fmt.Print(result) } } func processFile(filename string) (string, error) { data, err := os.ReadFile(filename) if err != nil { return "", err } // Здесь ваша логика обработки return string(data), nil }

Чек-лист хорошего CLI

  1. --help / -h — показывает справку с описанием, примерами и списком флагов
  2. --version — показывает версию программы
  3. Ошибки в stderrfmt.Fprintln(os.Stderr, ...)
  4. Правильные exit-коды — 0 при успехе, 1 при ошибке, 2 при неправильном использовании
  5. Понятные сообщения об ошибках — не просто "error", а что именно пошло не так
  6. Поддержка пайпов — возможность использовать в цепочках команд

Альтернативы пакету flag

Для простых CLI встроенного flag достаточно. Для сложных инструментов с подкомандами (как git commit, docker run) есть библиотеки:

БиблиотекаОсобенности
cobraСтандарт индустрии. Подкоманды, автодополнение, генерация документации. Используется в Kubernetes, Hugo, GitHub CLI
urfave/cliПроще cobra, но тоже поддерживает подкоманды
kongДекларативный подход через структуры

Но начинайте с flag — его достаточно для большинства задач, и он не требует внешних зависимостей.

Шпаргалка

// Определение флагов name := flag.String("name", "default", "Описание") port := flag.Int("port", 8080, "Порт") verbose := flag.Bool("v", false, "Подробный вывод") timeout := flag.Duration("timeout", 30*time.Second, "Таймаут") // Привязка к переменной var debug bool flag.BoolVar(&debug, "debug", false, "Режим отладки") // Кастомный --help flag.Usage = func() { fmt.Fprintf(os.Stderr, "Описание программы\n\nФлаги:\n") flag.PrintDefaults() } // Парсинг flag.Parse() // Оставшиеся аргументы args := flag.Args() // []string nArgs := flag.NArg() // количество firstArg := flag.Arg(0) // первый аргумент // Exit-коды os.Exit(0) // успех os.Exit(1) // ошибка os.Exit(2) // неправильное использование

Полезные ресурсы

Следующий шаг после статьи

Закрепите тему во вводном проекте без регистрации, а затем переходите к курсам.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи