Утилиты командной строки редко можно использовать в готовом виде без дополнительной настройки. Хорошие значения по умолчанию очень важны, однако полезные утилиты должны принимать конфигурацию от пользователей. В большинстве платформ утилиты командной строки принимают флаги для настройки выполнения команд. Флаги — это ограниченные значениями ключей строки, добавляемые после имени команды. Go позволяет настраивать утилиты командной строки, принимающие флаги посредством пакета flag
из стандартной библиотеки.
В этом обучающем руководстве мы покажем различные способы использования пакета flag
для построения различных видов утилит командной строки. Флаги используются для контроля вывода программы, ввода аргументов позиционирования с сочетанием флагов и других данных, а также для реализации субкоманд.
Использование пакета flag
предусматривает три шага. Вначале определяются переменные для сбора значений флагов, затем определяются флаги для использования приложением Go, и в заключение производится проверка синтаксиса флагов, переданных приложению при выполнении. Большинство функций в пакете flag
связаны с определением флагов и их привязке к определенным вами переменным. Синтаксическая проверка выполняется функцией Parse().
Для иллюстрации мы создадим программу, определяющую флаг Boolean для изменения сообщения, которое потом выводится стандартным способом. Если используется флаг -color
, программа выводит сообщение синим цветом. Если флаг не указан, сообщение выводится без цветов.
Создайте новый файл с именем boolean.go
:
- nano boolean.go
Добавьте в файл следующий код для создания программы:
package main
import (
"flag"
"fmt"
)
type Color string
const (
ColorBlack Color = "\u001b[30m"
ColorRed = "\u001b[31m"
ColorGreen = "\u001b[32m"
ColorYellow = "\u001b[33m"
ColorBlue = "\u001b[34m"
ColorReset = "\u001b[0m"
)
func colorize(color Color, message string) {
fmt.Println(string(color), message, string(ColorReset))
}
func main() {
useColor := flag.Bool("color", false, "display colorized output")
flag.Parse()
if *useColor {
colorize(ColorBlue, "Hello, DigitalOcean!")
return
}
fmt.Println("Hello, DigitalOcean!")
}
В этом примере используются управляющие последовательности ANSI, предписывающие терминалу выводить цветной текст. Это специальные последовательности символов, поэтому для них полезно определить новый тип. В этом примере мы присвоили типу имя Color
и определили его как string
. Затем мы определили палитру цветов для использования в следующем блоке const
. Функция colorize
, определяемая после блока const
, принимает одну из констант Color
и переменную string
для сообщения, которому назначается цвет. Затем она предписывает терминалу изменить цвет, сначала выводя управляющую последовательность для запрошенного цвета, а затем выводя сообщение, и, наконец, запрашивает сброс цвета терминала посредством вывода специальной последовательности сброса цвета.
В main
мы используем функцию flag.Bool
для определения флага логического оператора с именем color
. Второй параметр этой функции, false
, задает значение этого флага по умолчанию, если оно отсутствует. Хотя вы можете ожидать иного, установка значения true
не меняет поведения, так как при задании флага оно становится ложным. Поэтому этот параметр почти всегда будет иметь значение false
с флагами логических операторов.
Последний параметр — это строка документации, которая может быть выведена как сообщение об использовании. Возвращаемое этой функцией значение представляет собой указатель на bool
. Функция flag.Parse
на следующей строке использует этот указатель, чтобы задать переменную bool
на базе флагов, передаваемых пользователем. Мы сможем проверить значение этого указателя bool
, сняв ссылку на указатель. Дополнительную информацию о переменных указателей можно найти в обучающем руководстве по указателям. Используя это логическое значение, мы можем вызывать функцию colorize
, если флаг -color
установлен, и вызывать переменную fmt.Println
, если флаг отсутствует.
Сохраните файл и запустите программу без флагов:
- go run boolean.go
Вывод должен выглядеть так:
OutputHello, DigitalOcean!
Теперь запустите эту программу еще раз с помощью флага -color
:
- go run boolean.go -color
При этом также будет выведен текст, но при этом синим цветом.
Флаги — не единственные значения, которые передаются командам. Также вы можете отправлять имена файлов или другие данные.
Обычно команды принимают несколько аргументов, которые выступают в качестве субъекта фокуса команды. Например, команда head
, которая распечатывает первые строки файла, часто вызывается как head example.txt
. Файл example.txt
представляет собой позиционный аргумент вызова команды head
.
Функция Parse()
продолжает выполнять синтаксическую проверку появляющихся флагов, пока не обнаружит аргумент, не являющийся флагом. Пакет flag
делает их доступными через функции Args()
и Arg()
.
В качестве иллюстрации построим упрощенную реализацию команды head
, отображающей первые несколько строк указанного файла:
Создайте новый файл head.go
и добавьте следующий код:
package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
)
func main() {
var count int
flag.IntVar(&count, "n", 5, "number of lines to read from the file")
flag.Parse()
var in io.Reader
if filename := flag.Arg(0); filename != "" {
f, err := os.Open(filename)
if err != nil {
fmt.Println("error opening file: err:", err)
os.Exit(1)
}
defer f.Close()
in = f
} else {
in = os.Stdin
}
buf := bufio.NewScanner(in)
for i := 0; i < count; i++ {
if !buf.Scan() {
break
}
fmt.Println(buf.Text())
}
if err := buf.Err(); err != nil {
fmt.Fprintln(os.Stderr, "error reading: err:", err)
}
}
Вначале мы определяем переменную count
, где будет храниться количество строк, которое программа должна считывать из файла. Мы определяем флаг -n
, используя flag.IntVar
, что отражает поведение первоначальной программы head
. Эта функция позволяет нам передать собственный указатель в переменную в отличие от функций flag
, у которых нет суффикса Var
. Помимо этой разницы остальные параметры flag.IntVar
соответствуют параметрам flag.Int
: имя флага, значение по умолчанию и описание. Как и в предыдущем примере, мы вызовем flag.Parse()
для обработки пользовательских данных.
Следующий раздел выполняет чтение файла. Мы определим переменную io.Reader
, для которой будет задан запрошенный пользователем файл или которой будут передаваться стандартные входные данные программы. В выражении if
мы используем функцию flag.Arg
для доступа к первому аргументу после всех флагов. Если пользователь указал имя файла, оно будет задано. В противном случае, это будет пустая строка (""
). Если имя файла имеется в наличии, мы используем функцию os.Open
для открытия файла и задаем предварительно определенный для этого файла io.Reader
. В противном случае мы используем os.Stdin
для считывания стандартных исходных данных.
В заключительном разделе используется *bufio.Scanner
, созданный с помощью bufio.NewScanner
, для считывания строк из переменной io.Reader
in
. Мы проводим итерацию до значения count
в цикле for
и вызываем break
, если при сканировании строчки buf.Scan
получается значение false
, показывающее, что количество строчек меньше, чем запрошенное пользователем.
Запустите эту программу и выведите содержимое записанного файла, используя head.go
в качестве аргумента файла:
- go run head.go -- head.go
Разделитель --
представляет собой специальный флаг, который распознается пакетом flag
и показывает, что за ним не идут другие аргументы флагов. При запуске этой команды выводится следующее:
Outputpackage main
import (
"bufio"
"flag"
Используйте определенный вами флаг -n
для изменения объема выводимых данных:
- go run head.go -n 1 head.go
Так выводится только выражение пакета:
Outputpackage main
Наконец, если программа определяет отсутствие аргументов позиции, она считывает исходные данные из стандартного источника, как и команда head
. Попробуйте запустить следующую команду:
- echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3
Результат должен выглядеть так:
Outputfish
lobsters
sharks
Поведение функций flag
, которое мы наблюдали, ограничено исследованием вызова всей команды. Такое поведение требуется не всегда, особенно если вы пишете инструмент командной строки, поддерживающий субкоманды.
В современных приложениях командной строки часто реализуются субкоманды, что позволяет объединить набор инструментов в одной команде. Самый известный инструмент, использующий такую схему, называется git
. При проверке такой команды как git init
командой является git
, а init
является субкомандой git
. Важная характеристика субкоманд заключается в том, что каждая субкоманда может иметь собственный набор флагов.
Приложения Go могут поддерживать субкоманды с собственным набором флагов, используя оператор типа flag.( *FlagSet)
. Для иллюстрации мы создадим программу, которая будет реализовать команду, используя две субкоманды с разными флагами.
Создайте новый файл с именем subcommand.go
и добавьте в него следующий код:
package main
import (
"errors"
"flag"
"fmt"
"os"
)
func NewGreetCommand() *GreetCommand {
gc := &GreetCommand{
fs: flag.NewFlagSet("greet", flag.ContinueOnError),
}
gc.fs.StringVar(&gc.name, "name", "World", "name of the person to be greeted")
return gc
}
type GreetCommand struct {
fs *flag.FlagSet
name string
}
func (g *GreetCommand) Name() string {
return g.fs.Name()
}
func (g *GreetCommand) Init(args []string) error {
return g.fs.Parse(args)
}
func (g *GreetCommand) Run() error {
fmt.Println("Hello", g.name, "!")
return nil
}
type Runner interface {
Init([]string) error
Run() error
Name() string
}
func root(args []string) error {
if len(args) < 1 {
return errors.New("You must pass a sub-command")
}
cmds := []Runner{
NewGreetCommand(),
}
subcommand := os.Args[1]
for _, cmd := range cmds {
if cmd.Name() == subcommand {
cmd.Init(os.Args[2:])
return cmd.Run()
}
}
return fmt.Errorf("Unknown subcommand: %s", subcommand)
}
func main() {
if err := root(os.Args[1:]); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
Эта программа разделена на несколько частей: функция main
, функция root
и отдельные функции для реализации субкоманды. Функция main
обрабатывает ошибки, возвращаемые командами. Если любая функция возвращает ошибку, выражение if
определит ее, распечатает ошибку и закроет программу с кодом состояния 1
, сообщающим операционной системе о возникновении ошибки. Внутри функции main
мы передаем все аргументы вызова программы в функцию root
. Удалим первый аргумент, представляющий собой имя программы (в предыдущих примерах ./subcommand
), выделив срез os.Args.
Функция root
определяет []Runner
, где определяются все субкоманды. Runner
— это интерфейс субкоманд, позволяющий функции root
получить имя субкоманды с помощью Name()
и сравнить его с содержанием переменной subcommand
. После обнаружения правильной субкоманды посредством итерации переменной cmds
мы инициализируем субкоманду с остальными аргументами и вызываем метод Run()
для этой команды.
Мы определяем только одну субкоманду, хотя данная структура позволяет легко создавать и другие команды. Экземпляр GreetCommand
создается с помощью NewGreetCommand
при создани нового *flag.FlagSet
с помощью flag.NewFlagSet
. flag.NewFlagSet
принимает два аргумента: имя набора флагов и стратегию отчета об ошибках проверки синтаксиса. Имя *flag.FlagSet
доступно с помощью flag.( *FlagSet).
Метод Name. Мы используем этот метод в (*GreetCommand). Name()
так, что имя субкоманды соответствует имени, которое мы присвоили *flag.FlagSet
. NewGreetCommand
также определяет флаг -name
аналогично предыдущим примерам, но вместо этого вызывает его как метод поля *flag.FlagSet
*GreetCommand
, gc.fs
. Когда функция root
вызывает метод Init()
команды *GreetCommand
, мы передаем указанные аргументы методу Parse
поля *flag.FlagSet
.
Субкоманды будет проще увидеть, если вы соберете эту программу и запустите ее. Выполните сборку программы:
- go build subcommand.go
Теперь запустите программу без аргументов:
- ./subcommand
Вы увидите следующий результат:
OutputYou must pass a sub-command
Теперь запустите команду с субкомандой greet
:
- ./subcommand greet
Результат будет выглядеть следующим образом:
OutputHello World !
Теперь используйте флаг -name
с greet
для указания имени:
- ./subcommand greet -name Sammy
Программа выведет следующее:
OutputHello Sammy !
В этом примере проиллюстрированы некоторые принципы структурирования больших приложений командной строки в Go. Наборы FlagSet
разработаны так, чтобы дать разработчикам больше контроля над местом и способом обработки флагов логикой синтаксической проверки флагов.
Флаги делают приложения более полезными в разных условиях, поскольку они дают пользователям контроль над способом выполнения программы. Очень важно дать пользователям полезные параметры по умолчанию, но также следует дать им возможностям изменять параметры, которые не подходят для их конкретной ситуации. Вы увидели, что пакет flag
предоставляет вашим пользователям гибкие возможности выбора конфигурации. Вы можете выбрать несколько простых флагов или создать разветвленный набор субкоманд. В любом случае, использование пакета flag
поможет вам выполнить сборку утилит в стиле длинной истории гибких инструментов командной строки с поддержкой сценариев.
Ознакомьтесь с серией статей по программированию на Go, чтобы узнать больше о языке программирования Go.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!