Предыдущая часть – Golang: Go in Practice – заметки на полях, часть 1 – введение.
Стандартная библиотека Go включает в себя пакеты для создания приложения с поддержкой опций командной строки.
В отличии от стандартных, принятых в Linux/BSD, стилей – в пакете из стандартной библиотеке Go используются одинарные или двойные дефисы как взаимозаменяемые.
Т.е. тут не будет возможность указать несколько опций, используя формат tar -xvpf
– для Go xvpf
будет являться одной опцией, а не набором опций для tar
– -x
, -v
, -p
и -f
.
Содержание
Пакет flag
Пакет flag
поддерживает несколько вариантов добавления опций.
flag.String()
Вариант первый – с помощью flag.String()
:
package main import ( "flag" "fmt" ) func main() { // to save the 'name' option value var name = flag.String("name", "World", "The 'name' option value") // parse flag's options flag.Parse() // print the 'name'`s value fmt.Printf("Hello, %s\n", *name) }
Тут в var name
создаём переменную, которая ссылается на объект flag.String
, которому передаются имя флага (“name“), значение по умолчанию (“World“), и строка для отображения в --help
.
Вызываем программу без опций:
[simterm]
$ go run cli.go Hello, World
[/simterm]
С опцией name
, которой передаём значение username:
[simterm]
$ go run cli.go --name username Hello, username
[/simterm]
Или с использованием одного дефиса:
[simterm]
$ go run cli.go -name username Hello, username
[/simterm]
И без передачи значения:
[simterm]
$ go run cli.go -name flag needs an argument: -name Usage of /tmp/go-build471367643/b001/exe/cli: -name string The 'name' option value (default "World") exit status 2
[/simterm]
Указатели
Пару слов об указателях. Есть достаточно понятный разбор указателей в C тут>>> – разницы между указателями в Go и C особо нет, так что можно ознакомиться.
Если кратко – в примере выше мы выполняем fmt.Printf("Hello, %s\n", *name)
– в модификатор %s
подставляем значение, которое находится по адресу памяти, которое хранится в переменной name
(которая, собственно, тут является указателем).
Можно получить это значение так:
... // print the 'name'`s value fmt.Printf("Hello, %s\n", *name) fmt.Printf("The 'name'`s address is %p\n", &name) fmt.Printf("The 'name'`s value is %p\n", name) }
Выполняем:
[simterm]
$ go run cli.go Hello, World The 'name'`s address is 0xc00000c028 The 'name'`s value is 0xc00000e200
[/simterm]
Тут 0xc00000c028 – адрес самой переменной name
, а 0xc00000e200 – адрес объекта flag.String()
.
Документация по модификаторам Printf()
– тут>>>.
flag.BoolVar()
Другой вариант использования флагов – с помощью flag.BoolVar()
:
package main import ( "flag" "fmt" ) var rus bool func init() { flag.BoolVar(&rus, "russian", false, "Use Russian language") flag.BoolVar(&rus, "r", false, "Use Russian language") } func main() { // to save the 'name' option value var name = flag.String("name", "World", "The 'name' option value") // parse flag's options flag.Parse() // print the 'name'`s value in Eng or Ru depending on the 'lang'`s value if rus == true { fmt.Printf("Привет, %s\n", *name) } else { fmt.Printf("Hello, %s\n", *name) } }
Тут добавляем объявление переменной rus
типа boolean
, затем вызываем init-функцию, в которой с помощью flag.BoolVar()
определяем значение для rus
в true или false, а затем в if/else
проверяем значение и выбираем действие:
[simterm]
$ go run cli.go Hello, World $ go run cli.go -r Привет, World $ go run cli.go -russian Привет, World
[/simterm]
Что бы вывести аргументы программы – можно использовать os.Args
:
package main import ( "flag" "fmt" "os" ) var rus bool func init() { flag.BoolVar(&rus, "russian", false, "Use Russian language") flag.BoolVar(&rus, "r", false, "Use Russian language") } func main() { // to save the 'name' option value var name = flag.String("name", "World", "The 'name' option value") // parse flag's options flag.Parse() fmt.Println("Args: ", os.Args) // print the 'name'`s value in Eng or Ru depending on the 'lang'`s value if rus == true { fmt.Printf("Привет, %s\n", *name) } else { fmt.Printf("Hello, %s\n", *name) } }
Результат:
[simterm]
$ go run cli.go -name user -r Args: [/tmp/go-build005430960/b001/exe/cli -name user -r] Привет, user
[/simterm]
Или получить срез – всё, кроме нулевого элемента, т.е. имени самой программы:
... fmt.Println("Args: ", os.Args[1:]) ...
Запускаем:
[simterm]
$ go run cli.go -name user -r Args: [-name user -r] Привет, user
[/simterm]
UNIX-style опции
gnuflag
Для создания нормальных опций – существует несколько внешних библиотек, одна из них – gnuflag
.
Для импорта этого пакета потребуется Bazaar – устанавливаем:
[simterm]
$ sudo pacman -S bzr
[/simterm]
Импортируем пакет:
[simterm]
$ go get launchpad.net/gnuflag
[/simterm]
Первое – gnuflag
позволяет использовать группировку опций.
Т.е. – при использовании flag
, если вызывать опции в виде -ab
они не будут интерпретированы как две опции -a
и -b
.
Например – добавим в предыдущий скрипт опцию -с
:
package main import ( "flag" "fmt" "os" "strings" ) var rus bool var ca bool func init() { flag.BoolVar(&rus, "russian", false, "Use Russian language") flag.BoolVar(&rus, "r", false, "Use Russian language") flag.BoolVar(&ca, "capital", false, "Use CAPITAL") flag.BoolVar(&ca, "c", false, "Use CAPITAL") } func main() { // to save the 'name' option value var name = flag.String("name", "World", "The 'name' option value") // parse flag's options flag.Parse() fmt.Println("Args: ", os.Args[1:]) // concatenate strings hello_ru := "Привет " + *name hello_en := "Hello " + *name if rus == true { //fmt.Printf("Привет, %s\n", *name) if ca == true { fmt.Println(strings.ToUpper(hello_ru)) } else { fmt.Println(hello_ru) } } else { //fmt.Printf("Hello, %s\n", &name) if ca == true { fmt.Println(strings.ToUpper(hello_en)) } else { fmt.Println(hello_en) } } }
Вызываем с передачей опций вместе:
[simterm]
$ go run cli.go -rc flag provided but not defined: -rc Usage of /tmp/go-build528120016/b001/exe/cli: -c Use CAPITAL -capital Use CAPITAL -name string The 'name' option value (default "World") -r Use Russian language -russian Use Russian language exit status 2
[/simterm]
Получаем ошибку.
По отдельности – они будут работать:
[simterm]
$ go run cli.go -r -c Args: [-r -c] ПРИВЕТ WORLD
[/simterm]
Использование gnuflag
аналогично flag
, с той разницей, что в Parse()
передаётся дополнительный аргумент для allowIntersperse
.
allowIntersperse
может принимать true или false, и определяет – будет ли Parse()
проверять флаги после первого позиционного аргумента.
Для начала – пример того, как gnuflag
работает с группировкой опций.
Используем пример с flags
, но заменим flags
на gnuflags
:
package main import ( "fmt" "launchpad.net/gnuflag" "os" "strings" ) var rus bool var ca bool //var name string func init() { gnuflag.BoolVar(&rus, "russian", false, "Use Russian language") gnuflag.BoolVar(&rus, "r", false, "Use Russian language") gnuflag.BoolVar(&ca, "capital", false, "Use CAPITAL") gnuflag.BoolVar(&ca, "c", false, "Use CAPITAL") } func main() { var name = gnuflag.String("name", "World", "The 'name' option value") // parse flag's options gnuflag.Parse(false) fmt.Println(os.Args) hello_ru := "Привет " + *name hello_en := "Hello " + *name fmt.Println(ca) // print the 'name'`s value in Eng or Ru depending on the 'lang'`s value if rus == true { //fmt.Printf("Привет, %s\n", *name) if ca == true { fmt.Println(strings.ToUpper(hello_ru)) } else { fmt.Println(hello_ru) } } else { //fmt.Printf("Hello, %s\n", &name) if ca == true { fmt.Println(strings.ToUpper(hello_en)) } else { fmt.Println(hello_en) } } }
Проверяем:
[simterm]
$ go run gnuflag_cli.go -rc --name User [/tmp/go-build804626272/b001/exe/gnuflag_cli -rc --name User] true ПРИВЕТ USER
[/simterm]
Тут обработались обе опции – и -r
, и -c
.
Теперь рассмотрим значение allowIntersperse
.
В случае с flags
– он не будет проверять опции после первого позиционного аргумента:
... func main() { // to save the 'name' option value var name = flag.String("name", "World", "The 'name' option value") // parse flag's options flag.Parse() fmt.Println("Args: ", os.Args[1:]) fmt.Printf("First arg: %s\n", os.Args[1]) hello_ru := "Привет " + *name hello_en := "Hello " + *name ...
[simterm]
$ go run cli.go first -r Args: [first -r] First arg: first Hello World
[/simterm]
Опция -r
игнорируется.
В случае с gnuflags
, если в Parse()
передаём false – он будет вести себя аналогично flags
:
... func main() { var name = gnuflag.String("name", "World", "The 'name' option value") // parse flag's options //gnuflag.Parse(true) gnuflag.Parse(false) fmt.Println(os.Args) fmt.Printf("First arg: %s\n", os.Args[1]) hello_ru := "Привет " + *name hello_en := "Hello " + *name ...
Проверяем:
[simterm]
$ go run gnuflag_cli.go first -r [/tmp/go-build346296451/b001/exe/gnuflag_cli first -r] First arg: first false Hello World
[/simterm]
-r
проигнорирована.
Передаём в Parse()
значение true:
... func main() { var name = gnuflag.String("name", "World", "The 'name' option value") // parse flag's options gnuflag.Parse(true) //gnuflag.Parse(false) ...
Проверяем:
[simterm]
$ go run gnuflag_cli.go first -r [/tmp/go-build024407980/b001/exe/gnuflag_cli first -r] First arg: first false Привет World
[/simterm]
Или все опции:
[simterm]
$ go run gnuflag_cli.go first -r --name User -c [/tmp/go-build386862510/b001/exe/gnuflag_cli first -r --name User -c] First arg: first true ПРИВЕТ USER
[/simterm]
Работает.
go-flags
go-flags
Ещё один пакет для работы с опциями командной строки – go-flags
.
В отличии от flags
и gnuflags
– тут опции задаются в тегах структуры, например:
package main import ( "fmt" flags "github.com/jessevdk/go-flags" ) var opts struct { Name string `short:"n" long:"name" default:"World" description:"The Name option value"` Rus bool `short:"r" long:"russian" description:"Use Russian language"` } func main() { _, err := flags.Parse(&opts) if err != nil { panic(err) } hello_ru := "Привет " + opts.Name hello_en := "Hello " + opts.Name if opts.Rus == true { fmt.Println(hello_ru) } else { fmt.Println(hello_en) } }
И примеры выполнения:
[simterm]
$ go run goflags.go Hello World $ go run goflags.go -r Привет World $ go run goflags.go -rn Username Привет Username $ go run goflags.go -r --name Username Привет Username $ go run goflags.go -h Usage: goflags [OPTIONS] Application Options: -n, --name= The Name option value (default: World) -r, --russian Use Russian language Help Options: -h, --help Show this help message panic: Usage: goflags [OPTIONS] Application Options: -n, --name= The Name option value (default: World) -r, --russian Use Russian language Help Options: -h, --help Show this help message
[/simterm]
cli.go
cli.go
cli.go
представляет собой фреймворк для создания CLI-приложений.
Используется в таких приложениях как Docker:
[simterm]
$ grep -r urfave docker-ce/ docker-ce/components/cli/vendor/github.com/opencontainers/runc/vendor.conf:github.com/urfave/cli d53eb991652b1d438abdd34ce4bfa3ef1539108e docker-ce/components/cli/vendor/github.com/moby/buildkit/vendor.conf:github.com/urfave/cli 7bc6a0acffa589f415f88aca16cc1de5ffd66f9c docker-ce/components/cli/vendor/github.com/containerd/containerd/vendor.conf:github.com/urfave/cli 7bc6a0acffa589f415f88aca16cc1de5ffd66f9c docker-ce/components/engine/vendor/github.com/opencontainers/runc/vendor.conf:github.com/urfave/cli d53eb991652b1d438abdd34ce4bfa3ef1539108e docker-ce/components/engine/vendor/github.com/moby/buildkit/vendor.conf:github.com/urfave/cli 7bc6a0acffa589f415f88aca16cc1de5ffd66f9c docker-ce/components/engine/vendor/github.com/containerd/cri/vendor.conf:github.com/urfave/cli 7bc6a0acffa589f415f88aca16cc1de5ffd66f9c docker-ce/components/engine/vendor/github.com/containerd/containerd/vendor.conf:github.com/urfave/cli 7bc6a0acffa589f415f88aca16cc1de5ffd66f9c
[/simterm]
Напишем такой пример:
package main import ( "fmt" "gopkg.in/urfave/cli.v1" "os" ) func main() { app := cli.NewApp() app.Name = "Example CLI" app.Usage = "Print Hello messages" app.Flags = []cli.Flag{ cli.StringFlag{ Name: "name, n", Value: "World", Usage: "The 'name' option value", }, cli.BoolFlag{ Name: "russian, r", Usage: "Use Russian language", }, } app.Action = func(c *cli.Context) error { hello_ru := "Привет " + c.GlobalString("name") hello_en := "Hello " + c.GlobalString("name") rus := c.GlobalBool("russian") if rus == true { fmt.Printf("%s\n", hello_ru) } else { fmt.Printf("%s\n", hello_en) } return nil } app.Run(os.Args) }
Тут:
cli.NewApp()
– создаём новый экземляр приложения, доступ к которому будет через переменную app- в
app.Flags
задаём опции приложенияStringFlag
– строковые флагиName
– короткое и длинное именаValue
– значение по умолчаниюUsage
– для вывода вhelp
BoolFlag
– булев (логический) флагName
– короткое и длинное именаUsage
– для вывода вhelp
- `Value` – не задаём явно: при вызове программы без опции – будет задано значение false, в противном случае true
app.Action()
– задаём действие по умолчанию, аргументом передаём cli.Context с флагамиc.GlobalString("name")
– получаем значение из опции name- аналогично –
c.GlobalBool("russian")
app.Run(os.Args)
– запускаем программу, передавая всё заданные в консоли аргументы
Проверяем:
[simterm]
$ go run go-cli-go.go Hello World $ go run go-cli-go.go -n User Hello User $ go run go-cli-go.go -n User -r Привет User
[/simterm]
Команды и подкоманды
Так же он поддерживает работу с командами и опциям команд.
Напишем программу, которая выполняет прямой и обратный отсчёт, и при этом умеет принимать аргумент, в котором можно указать число для начала отсчёта:
package main import ( "fmt" "gopkg.in/urfave/cli.v1" "os" ) func main() { app := cli.NewApp() app.Usage = "Print Hello messages" app.Commands = []cli.Command{ { Name: "up", ShortName: "u", Usage: "Count Up", Flags: []cli.Flag{ cli.IntFlag{ Name: "stop, s", Usage: "Value to count up to", Value: 10, }, }, Action: func(c *cli.Context) error { start := c.Int("stop") if start <= 0 { fmt.Println("Stop cannot be negative") } for i := 1; i <= start; i++ { fmt.Println("Value: ", i) } return nil }, }, { Name: "down", ShortName: "d", Usage: "Count Down", Flags: []cli.Flag{ cli.IntFlag{ Name: "start, s", Usage: "Value to count down from", Value: 10, }, }, Action: func(c *cli.Context) error { start := c.Int("start") if start <= 0 { fmt.Println("Start cannot be negative") } for i := start; i >= 0; i-- { fmt.Println("Value: ", i) } return nil }, }, } app.Run(os.Args) }
Подробно тут уже останавливаться не буду, проверяем работу:
[simterm]
$ go run go-cli-go-subcmds.go NAME: go-cli-go-subcmds - Print Hello messages USAGE: go-cli-go-subcmds [global options] command [command options] [arguments...] VERSION: 0.0.0 COMMANDS: up, u Count Up down, d Count Down help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --help, -h show help --version, -v print the version [/simterm] [simterm] $ go run go-cli-go-subcmds.go up Value: 1 Value: 2 Value: 3 Value: 4 ... Value: 8 Value: 9 Value: 10 [/simterm] [simterm] $ go run go-cli-go-subcmds.go up -s 20 Value: 1 Value: 2 Value: 3 ... Value: 18 Value: 19 Value: 20 [/simterm] [simterm] $ go run go-cli-go-subcmds.go down Value: 10 Value: 9 Value: 8 ... Value: 2 Value: 1 Value: 0 [/simterm] [simterm] $ go run go-cli-go-subcmds.go down -s 20 Value: 20 Value: 19 Value: 18 ... Value: 1 Value: 0
[/simterm]
В целом – этого достаточно, что использовать опции командной строки в своих программах.