Создание 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
--help/-h— показывает справку с описанием, примерами и списком флагов--version— показывает версию программы- Ошибки в stderr —
fmt.Fprintln(os.Stderr, ...) - Правильные exit-коды — 0 при успехе, 1 при ошибке, 2 при неправильном использовании
- Понятные сообщения об ошибках — не просто "error", а что именно пошло не так
- Поддержка пайпов — возможность использовать в цепочках команд
Альтернативы пакету 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) // неправильное использование
Полезные ресурсы
- Пакет flag — официальная документация
- Command Line Interface Guidelines — принципы дизайна хороших CLI
- 12 Factor CLI Apps — лучшие практики
- Cobra — документация популярной CLI-библиотеки