Golang: Go in Practice — заметки на полях, часть 2 — CLI приложение на Go

Автор: | 02/14/2019
 

Предыдущая часть — 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.

Вызываем программу без опций:

go run cli.go
Hello, World

С опцией name, которой передаём значение username:

go run cli.go --name username
Hello, username

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

go run cli.go -name username
Hello, username

И без передачи значения:

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
Указатели

Пару слов об указателях. Есть достаточно понятный разбор указателей в 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)
}

Выполняем:

go run cli.go
Hello, World
The 'name'`s address is 0xc00000c028
The 'name'`s value is 0xc00000e200

Тут 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 проверяем значение и выбираем действие:

go run cli.go
Hello, World
go run cli.go -r
Привет, World
go run cli.go -russian                                                                                                                                                                    
Привет, World

Что бы вывести аргументы программы — можно использовать 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)
    }
}

Результат:

go run cli.go -name user -r
Args:  [/tmp/go-build005430960/b001/exe/cli -name user -r]
Привет, user

Или получить срез — всё, кроме нулевого элемента, т.е. имени самой программы:

...
fmt.Println("Args: ", os.Args[1:])
...

Запускаем:

go run cli.go -name user -r
Args:  [-name user -r]
Привет, user

UNIX-style опции

gnuflag

Для создания нормальных опций — существует несколько внешних библиотек, одна из них —  gnuflag.

Для импорта этого пакета потребуется Bazaar — устанавливаем:

sudo pacman -S bzr

Импортируем пакет:

go get launchpad.net/gnuflag

Первое — 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)
        }
    }
}

Вызываем с передачей опций вместе:

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

Получаем ошибку.

По отдельности — они будут работать:

go run cli.go -r -c
Args:  [-r -c]
ПРИВЕТ WORLD

Использование 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)
        }
    }
}

Проверяем:

go run gnuflag_cli.go -rc --name User
[/tmp/go-build804626272/b001/exe/gnuflag_cli -rc --name User]
true
ПРИВЕТ USER

Тут обработались обе опции — и -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
...
go run cli.go first -r
Args:  [first -r]
First arg: first
Hello World

Опция -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
...

Проверяем:

go run gnuflag_cli.go first -r
[/tmp/go-build346296451/b001/exe/gnuflag_cli first -r]
First arg: first
false
Hello World

-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)
...

Проверяем:

go run gnuflag_cli.go first -r
[/tmp/go-build024407980/b001/exe/gnuflag_cli first -r]
First arg: first
false
Привет World

Или все опции:

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

Работает.

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)
    }
}

И примеры выполнения:

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

cli.go

cli.go представляет собой фреймворк для создания CLI-приложений.

Используется в таких приложениях как Docker:

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

Напишем такой пример:

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) — запускаем программу, передавая всё заданные в консоли аргументы

Проверяем:

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
Команды и подкоманды

Так же он поддерживает работу с командами и опциям команд.

Напишем программу, которая выполняет прямой и обратный отсчёт, и при этом умеет принимать аргумент, в котором можно указать число для начала отсчёта:

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)
}

Подробно тут уже останавливаться не буду, проверяем работу:

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
go run go-cli-go-subcmds.go up
Value:  1
Value:  2
Value:  3
Value:  4
...
Value:  8
Value:  9
Value:  10
go run go-cli-go-subcmds.go up -s 20
Value:  1
Value:  2
Value:  3
...
Value:  18
Value:  19
Value:  20
go run go-cli-go-subcmds.go down
Value:  10
Value:  9
Value:  8
...
Value:  2
Value:  1
Value:  0
go run go-cli-go-subcmds.go down -s 20
Value:  20
Value:  19
Value:  18
...
Value:  1
Value:  0

В целом — этого достаточно, что использовать опции командной строки в своих программах.