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

Автор: | 14/02/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.

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

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

В отличии от 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-приложений.

Используется в таких приложениях как 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]

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