Введение

Утилиты командной строки редко можно использовать в готовом виде без дополнительной настройки. Хорошие значения по умолчанию очень важны, однако полезные утилиты должны принимать конфигурацию от пользователей. В большинстве платформ утилиты командной строки принимают флаги для настройки выполнения команд. Флаги — это ограниченные значениями ключей строки, добавляемые после имени команды. Go позволяет настраивать утилиты командной строки, принимающие флаги посредством пакета flag из стандартной библиотеки.

В этом обучающем руководстве мы покажем различные способы использования пакета flag для построения различных видов утилит командной строки. Флаги используются для контроля вывода программы, ввода аргументов позиционирования с сочетанием флагов и других данных, а также для реализации субкоманд.

Использование флага для изменения поведения программы

Использование пакета flag предусматривает три шага. Вначале определяются переменные для сбора значений флагов, затем определяются флаги для использования приложением Go, и в заключение производится проверка синтаксиса флагов, переданных приложению при выполнении. Большинство функций в пакете flag связаны с определением флагов и их привязке к определенным вами переменным. Синтаксическая проверка выполняется функцией Parse().

Для иллюстрации мы создадим программу, определяющую флаг Boolean для изменения сообщения, которое потом выводится стандартным способом. Если используется флаг -color, программа выводит сообщение синим цветом. Если флаг не указан, сообщение выводится без цветов.

Создайте новый файл с именем boolean.go:

  • nano boolean.go

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

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

Вывод должен выглядеть так:

Output
Hello, DigitalOcean!

Теперь запустите эту программу еще раз с помощью флага -color:

  • go run boolean.go -color

При этом также будет выведен текст, но при этом синим цветом.

Флаги — не единственные значения, которые передаются командам. Также вы можете отправлять имена файлов или другие данные.

Работа с позиционными аргументами

Обычно команды принимают несколько аргументов, которые выступают в качестве субъекта фокуса команды. Например, команда head, которая распечатывает первые строки файла, часто вызывается как head example.txt. Файл example.txt представляет собой позиционный аргумент вызова команды head.

Функция Parse() продолжает выполнять синтаксическую проверку появляющихся флагов, пока не обнаружит аргумент, не являющийся флагом. Пакет flag делает их доступными через функции Args() и Arg().

В качестве иллюстрации построим упрощенную реализацию команды head, отображающей первые несколько строк указанного файла:

Создайте новый файл head.go и добавьте следующий код:

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 и показывает, что за ним не идут другие аргументы флагов. При запуске этой команды выводится следующее:

Output
package main import ( "bufio" "flag"

Используйте определенный вами флаг -n для изменения объема выводимых данных:

  • go run head.go -n 1 head.go

Так выводится только выражение пакета:

Output
package main

Наконец, если программа определяет отсутствие аргументов позиции, она считывает исходные данные из стандартного источника, как и команда head. Попробуйте запустить следующую команду:

  • echo "fish\nlobsters\nsharks\nminnows" | go run head.go -n 3

Результат должен выглядеть так:

Output
fish lobsters sharks

Поведение функций flag, которое мы наблюдали, ограничено исследованием вызова всей команды. Такое поведение требуется не всегда, особенно если вы пишете инструмент командной строки, поддерживающий субкоманды.

Использование FlagSet для реализации субкоманд

В современных приложениях командной строки часто реализуются субкоманды, что позволяет объединить набор инструментов в одной команде. Самый известный инструмент, использующий такую схему, называется 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

Вы увидите следующий результат:

Output
You must pass a sub-command

Теперь запустите команду с субкомандой greet:

  • ./subcommand greet

Результат будет выглядеть следующим образом:

Output
Hello World !

Теперь используйте флаг -name с greet для указания имени:

  • ./subcommand greet -name Sammy

Программа выведет следующее:

Output
Hello Sammy !

В этом примере проиллюстрированы некоторые принципы структурирования больших приложений командной строки в Go. Наборы FlagSet разработаны так, чтобы дать разработчикам больше контроля над местом и способом обработки флагов логикой синтаксической проверки флагов.

Заключение

Флаги делают приложения более полезными в разных условиях, поскольку они дают пользователям контроль над способом выполнения программы. Очень важно дать пользователям полезные параметры по умолчанию, но также следует дать им возможностям изменять параметры, которые не подходят для их конкретной ситуации. Вы увидели, что пакет flag предоставляет вашим пользователям гибкие возможности выбора конфигурации. Вы можете выбрать несколько простых флагов или создать разветвленный набор субкоманд. В любом случае, использование пакета flag поможет вам выполнить сборку утилит в стиле длинной истории гибких инструментов командной строки с поддержкой сценариев.

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

0 Comments

Creative Commons License