Menu

Аргументы командной строки в Golang: os.Args, flag и env

Как программа на Go читает командную строку: os.Args, пакет flag для типизированных опций, подкоманды через FlagSet, переменные окружения через os.Getenv и os.LookupEnv и коды выхода через os.Exit.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

os.Args

os.Args это слайс строк. os.Args[0] это имя программы, остальное это аргументы ровно в том виде, в каком их передала оболочка.

В панели Args редактора каждое поле это один аргумент, который передаётся как есть. Введите hello, two words и -v в три поля: программа увидит три аргумента, и two words останется одним аргументом с пробелом внутри. В терминале разбиение делает оболочка, поэтому то же самое выглядит как go run . hello "two words" -v.

Всегда проверяйте len(os.Args) перед обращением по индексу. os.Args[1] без аргументов паникует с index out of range [1] with length 1.

Аргументы это строки. Числа преобразуйте через strconv.Atoi или strconv.ParseFloat и обрабатывайте ошибку, ведь пользователи вводят что угодно.

Пакет flag

Для опций вроде -port 8080 -verbose используйте flag. Он разбирает аргументы, преобразует типы, сообщает об ошибках и генерирует справку.

Без аргументов программа один раз печатает hello, world. В панели Args введите -name и Gopher в два поля, а затем ещё -count=3, -loud и extra в три. Программа трижды напечатает HELLO, GOPHER! и remaining args: [extra].

Как работают флаги:

  • Каждая функция объявления (flag.String, flag.Int, flag.Bool, flag.Float64, flag.Duration, flag.Uint64...) принимает имя, значение по умолчанию и строку справки и возвращает указатель. Читайте значение через *name после flag.Parse().
  • Формы с Var привязываются к уже существующей переменной: flag.IntVar(&cfg.Port, "port", 8080, "port"). Для структуры конфигурации так аккуратнее.
  • Пользователи могут писать -name value, -name=value, --name value или --name=value. Go не различает один и два дефиса.
  • Булевым флагам для значения нужен =. -loud ставит true, -loud=false ставит false, а -loud false ставит true и оставляет false позиционным аргументом.
  • Разбор останавливается на первом аргументе, который не флаг (или на --). prog file.txt -v считает -v позиционным аргументом. Ставьте флаги первыми.
  • flag.Args() возвращает оставшиеся позиционные аргументы, flag.NArg() их число, а flag.Arg(i) один из них.

Неизвестный флаг или неверное значение печатают ошибку и справку и завершают программу с кодом 2. -h или -help печатает справку и завершает с кодом 0 (начиная с Go 1.15). Текст справки генерируется из ваших объявлений:

Usage of greet:
  -count int
    	how many times (default 1)
  -delay duration
    	pause between greetings, e.g. 10ms
  -loud
    	shout the greeting
  -name string
    	who to greet (default "world")

Присвойте flag.Usage свою функцию, чтобы напечатать собственный заголовок перед вызовом flag.PrintDefaults().

Подкоманды через FlagSet

У утилит вроде git commit -m msg есть подкоманды со своими флагами. Создайте flag.FlagSet на каждую подкоманду и выбирайте нужную через switch по первому аргументу:

flag.ContinueOnError заставляет Parse вернуть ошибку, а не завершать программу, и это оставляет функцию тестируемой. run принимает аргументы параметром, а не читает os.Args, поэтому тест может вызвать run([]string{"list", "-all"}) напрямую. Попробуйте в панели Args list и -all или delete, чтобы увидеть путь с ошибкой.

Для больших CLI с вложенными командами, автодополнением в оболочке и генерируемой документацией большинство проектов используют сторонний github.com/spf13/cobra. Для маленьких утилит стандартного пакета flag вполне хватает.

Переменные окружения

  • os.Getenv возвращает "" и когда переменной нет, и когда она задана пустой строкой. os.LookupEnv их различает.
  • Значения всегда строки. Преобразуйте и проверяйте их при запуске и падайте с понятным сообщением, а не посреди обработки запроса.
  • os.Setenv влияет на текущий процесс и на дочерние процессы, запущенные позже. Окружение оболочки, которая вас запустила, он изменить не может.
  • os.Environ() возвращает все переменные в виде строк "KEY=value".

Распространённая схема конфигурации: флаги для того, что вводит человек, переменные окружения для настроек развёртывания (их задают контейнерные платформы), причём флаги переопределяют окружение, а окружение переопределяет значения по умолчанию.

Коды выхода и os.Exit

Программа на Go завершается с кодом 0, когда main возвращает управление. os.Exit(code) сразу завершает процесс с этим кодом. По соглашению 0 это успех, 1 общая ошибка, а 2 ошибка использования (так делает пакет flag).

os.Exit не выполняет отложенные функции. Файлы не сбрасываются на диск, а очистка в defer пропускается. log.Fatal вызывает os.Exit(1) с тем же эффектом. Держите os.Exit в одном месте, в конце main, а саму программу поместите в функцию run, которая возвращает ошибку, как в примере с подкомандами:

func main() {
	if err := run(os.Args[1:]); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}

Пишите сообщения об ошибках в os.Stderr, а не в os.Stdout, чтобы они оставались видны, когда вывод перенаправлен в файл или передан другой команде. Неперехваченная паника завершает программу с кодом 2.

Частые ошибки

  • Обращение к os.Args по индексу без проверки длины. Отсутствующие аргументы вызывают панику.
  • Чтение флагов до flag.Parse(). Вы получите значения по умолчанию.
  • Забытая *. fmt.Println(port) печатает адрес вроде 0xc000012345, а не значение.
  • Флаги после позиционных аргументов. prog input.txt -v не разберёт -v.
  • -verbose false для булева флага. Пишите -verbose=false.
  • os.Exit или log.Fatal глубоко внутри программы. Отложенная очистка не выполнится, а код нельзя будет протестировать. Возвращайте ошибки наверх до main.

Часто задаваемые вопросы

Как получить аргументы командной строки в Go?

os.Args это []string, где под индексом 0 лежит имя программы, а за ним аргументы. os.Args[1:] это аргументы, которые ввёл пользователь. Проверяйте len(os.Args) перед обращением по индексу, иначе программа запаникует при отсутствии аргумента.

Как пользоваться пакетом flag в Go?

Объявите флаги, вызовите flag.Parse(), затем читайте их: port := flag.Int("port", 8080, "port to listen on"), flag.Parse(), fmt.Println(*port). Функции возвращают указатели. Пользователи пишут -port=9000, -port 9000 или --port 9000, а -h печатает сгенерированную справку.

Как прочитать переменную окружения в Go?

os.Getenv("HOME") возвращает значение или пустую строку, если переменная не задана. Чтобы отличить «не задана» от «задана пустой», используйте v, ok := os.LookupEnv("HOME"). os.Setenv меняет окружение текущего процесса и дочерних процессов, запущенных после этого.

Выполняет ли os.Exit отложенные функции в Go?

Нет. os.Exit сразу завершает процесс с указанным кодом, и отложенные вызовы не выполняются, так что буферизованный вывод может пропасть, а файлы не сбросятся на диск. Распространённый шаблон: func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) } }, где вся настоящая работа и все defer находятся внутри run.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ