Как использовать флаговый пакет в Go

Вступление

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

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

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

Использование пакетаflag включает три шага: во-первых,define variables для захвата значений флагов, затем определение флагов, которые ваше приложение 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 Escape Sequences используется для указания терминалу отображать цветной вывод. Это специализированные последовательности символов, поэтому имеет смысл определить для них новый тип. В этом примере мы назвали этот типColor и определили тип какstring. Затем мы определяем палитру цветов для использования в следующем блокеconst. Функцияcolorize, определенная после блокаconst, принимает одну из этих константColor и переменнуюstring для раскрашивания сообщения. Затем он инструктирует терминал изменить цвет, сначала печатая escape-последовательность для запрошенного цвета, затем печатает сообщение и, наконец, запрашивает, чтобы терминал сбросил свой цвет, печатая специальную последовательность сброса цвета.

Внутриmain мы используем функциюflag.Bool, чтобы определить логический флагcolor. Второй параметр этой функции,false, устанавливает значение по умолчанию для этого флага, если он не указан. Вопреки вашим ожиданиям, установка этого параметра наtrue не меняет поведение, так что предоставление флага приведет к его ложному состоянию. Следовательно, значение этого параметра почти всегда равноfalse с логическими флагами.

Последний параметр - это строка документации, которая может быть напечатана в виде сообщения об использовании. Значение, возвращаемое этой функцией, является указателем наbool. Функцияflag.Parse в следующей строке использует этот указатель для установки переменнойbool на основе флагов, переданных пользователем. Затем мы можем проверить значение этого указателяbool, разыменовав указатель. Более подробную информацию о переменных-указателях можно найти вtutorial on pointers. Используя это логическое значение, мы можем затем вызвать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 и добавьте следующий код:

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. Эта функция позволяет нам передавать наши собственныеpointer в переменную в отличие от функцийflag, у которых нет суффиксаVar. Помимо этого различия, остальные параметрыflag.IntVar следуют его аналогуflag.Int: имя флага, значение по умолчанию и описание. Затем, как и в предыдущем примере, мы вызываемflag.Parse() для обработки ввода пользователя.

Следующий раздел читает файл. Сначала мы определяем переменнуюio.Reader, которая будет либо установлена ​​в файл, запрошенный пользователем, либо будет передаваться программе стандартный ввод. В оператореif мы используем функциюflag.Arg для доступа к первому позиционному аргументу после всех флагов. Если пользователь указал имя файла, оно будет установлено. В противном случае это будет пустая строка (""). Когда присутствует имя файла, мы используем функциюos.Open, чтобы открыть этот файл и установитьio.Reader, которые мы определили ранее, для этого файла. В противном случае мы используемos.Stdin для чтения из стандартного ввода.

В последнем разделе используется*bufio.Scanner, созданный с помощьюbufio.NewScanner, для чтения строк из переменнойio.Readerin. Мы выполняем итерацию до значенияcount, используяfor loop, вызывая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, которые вы видели до сих пор, ограничивалось проверкой всего вызова команды. Вы не всегда хотите такое поведение, особенно если вы пишете инструмент командной строки, который поддерживает подкоманды.

Использование 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 обрабатывает ошибки, возвращаемые командами. Если какая-либо функция возвращаетerror, операторif поймает это, распечатает ошибку, и программа выйдет с кодом состояния1, указывая, что ошибка произошла с остальными операционной системы. Вmain мы передаем все аргументы, с которыми была вызвана программа, вroot. Мы удаляем первый аргумент, который является именем программы (в предыдущих примерах./subcommand), сначала нарезавos.Args.

Функцияroot определяет[]Runner, где будут определены все подкоманды. Runner - этоinterface для подкоманд, который позволяет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`s разработаны, чтобы дать разработчикам больше контроля над тем, где и как флаги обрабатываются логикой анализа флагов.

Заключение

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

Чтобы узнать больше о языке программирования Go, ознакомьтесь с нашим полнымHow To Code in Go series.