Tutorial

Como usar o pacote Flag em Go

GoDevelopment

Introdução

Os utilitários de linha de comando raramente são úteis, pois não vêm prontos para usar e necessitam de configuração adicional. Padrões bons são importantes, mas os utilitários úteis precisam aceitar a configuração dos usuários. Na maioria das plataformas, os utilitários de linha de comando aceitam sinalizadores (flags) que permitem personalizar a execução do comando. Os sinalizadores são strings de valores-chave delimitados, adicionadas após o nome do comando. A linguagem Go permite que você crie utilitários de linha de comando que aceitam sinalizadores, usando o pacote flag da biblioteca padrão.

Neste tutorial, serão explorados várias maneiras de usar o pacote flag para construir diferentes tipos de utilitários de linha de comando. Você usará um sinalizador para controlar o resultado do programa, introduzir argumentos posicionais - nos quais você mistura sinalizadores e outros dados e, em seguida, implementar subcomandos.

Usando um sinalizador para alterar o comportamento de um programa

Usar o pacote flag envolve três passos: primeiro, defina as variáveis para capturar os valores de sinalizador; em seguida, defina os sinalizadores que o seu aplicativo em Go usará e, por fim, analise os sinalizadores fornecidos para o aplicativo na execução. A maioria das funções dentro do pacote flag diz respeito à definição dos sinalizadores e de conectá-los às variáveis que você tiver definido. A fase de análise é manipulada pela função Parse().

Para ilustrar, você criará um programa que define um sinalizador Booleano, o qual altera a mensagem que será impressa em um resultado padrão. Se houver um sinalizador -color fornecido, o programa irá imprimir uma mensagem em azul. Se nenhum sinalizador for fornecido, a mensagem será impressa sem qualquer cor.

Crie um novo arquivo chamado boolean.go:

  • nano boolean.go

Adicione o código a seguir ao arquivo para criar o programa:

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

Esse exemplo usa Sequências de Escape ANSI para instruir o terminal a exibir um resultado colorido. Essas são sequências especializadas de caracteres, de modo que faz sentido definir um novo tipo para elas. Nesse exemplo, chamamos àquele tipo de Color e definimos o tipo como uma string. Então, definimos uma paleta de cores para usar no bloco const que segue. A função colorize, definida após o bloco const, aceita uma dessas constantes Color e uma variável string para que a mensagem seja colorida. Depois, a função diz ao terminal para alterar a cor, imprimindo primeiro a sequência de escape da cor solicitada; em seguida, imprime a mensagem e, por fim, solicita que o terminal redefina sua cor, imprimindo a sequência especial para redefinição de cores.

Dentro do main, usamos a função flag.Bool para definir um sinalizador booleano chamado color. O segundo parâmetro para esta função, false, define o valor padrão para esse sinalizador, quando não for fornecido. Ao contrário das expectativas que você possa ter, definir o parâmetro como true não reverte o comportamento na medida em que o fornecimento de um sinalizador fará com que um parâmetro se torne falso. Consequentemente, o valor desse parâmetro é quase sempre false com os sinalizadores booleanos.

O parâmetro final é uma string de documentação que pode ser impressa como uma mensagem de uso. O valor retornado dessa função é um ponteiro para um bool. A função flag.Parse, na linha seguinte, usa esse ponteiro para definir a variável bool com base nos sinalizadores transmitidos pelo usuário. Depois disso, conseguiremos verificar o valor desse ponteiro bool, desreferenciando o ponteiro. No tutorial sobre ponteiros, você encontra mais informações sobre variáveis de ponteiros. Usando esse valor Booleano, podemos então chamar colorize quando um sinalizador -color estiver definido e chamar a variável fmt.Println quando o sinalizador estiver ausente.

Salve o arquivo e execute o programa sem nenhum sinalizador:

  • go run boolean.go

Você verá o seguinte resultado:

Output
Hello, DigitalOcean!

Agora, execute este programa novamente com o sinalizador -color:

  • go run boolean.go -color

O resultado será o mesmo texto, mas, dessa vez, na cor azul.

Os sinalizadores não são os únicos valores enviados para os comandos. Você também pode enviar nomes de arquivo ou outros dados.

Trabalhando com os argumentos posicionais

Normalmente, os comandos receberão uma série de argumentos que agem como o assunto que o comando tem em foco. Por exemplo, o comando head - que imprime as primeiras linhas de um arquivo - com frequência, é invocado como head example.txt. O arquivo example.txt é um argumento posicional na invocação do comando head.

A função Parse() continuará a analisar os sinalizadores que encontrar, até detectar um argumento não sinalizador. O pacote flag os disponibiliza através das funções Args() e Arg().

Para demonstrar isso, você compilará uma reimplementação simplificada do comando head, o qual exibe várias das primeiras linhas de um determinado arquivo:

Crie um novo arquivo chamado head.go e adicione o seguinte código:

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

Primeiro, definimos uma variável count para reter o número de linhas que o programa deverá ler do arquivo. Então, definimos um sinalizador -n, usando a flag.IntVar e espelhando o comportamento do programa original head. Essa função nos permite enviar nosso próprio ponteiro para uma variável, em contraste com as funções flag que não têm o sufixo Var. Além dessa diferença, os demais parâmetros para a flag.IntVar seguem sua contraparte flag.Int: o nome do sinalizador, um valor padrão e uma descrição. Como no exemplo anterior, chamamos o flag.Parse() para processar a entrada do usuário.

A próxima seção lê o arquivo. Primeiro, definimos uma variável io.Reader que será definida para o arquivo solicitado pelo usuário, ou para a entrada padrão enviada para o programa. Dentro da instrução if, usamos a função flag.Arg para acessar o primeiro argumento posicional depois de todos os sinalizadores. Se o usuário forneceu um nome de arquivo, este estará definido. Caso contrário, será a string vazia (""). Quando um nome de arquivo estiver presente, usamos a função os.Open para abrir aquele arquivo e definir o io.Reader que definimos anteriormente para aquele arquivo. Caso contrário, usamos o os.Stdin para ler a partir da entrada padrão.

A seção final usa um *bufio.Scanner, criado com o bufio.NewScanner para ler linhas da entrada in da variável io.Reader. Iteramos até o valor da count usando um loop for, que chamará o break caso a verificação da linha com o buf.Scan produza um valor false, indicando que o número de linhas é menor do que o número solicitado pelo usuário.

Execute este programa e exiba o conteúdo do arquivo que acabou de escrever, usando o head.go como o argumento do arquivo:

  • go run head.go -- head.go

O separador -- é um sinalizador especial reconhecido pelo pacote flag, o qual indica que não haverá mais argumentos de sinalização na sequência. Quando executar este comando, você recebe o seguinte resultado:

Output
package main import ( "bufio" "flag"

Use o sinalizador -n que você definiu para ajustar a quantidade de resultado:

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

Isso gera apenas a instrução do pacote:

Output
package main

Por fim, quando o programa detecta que nenhum argumento posicional foi fornecido, ele lê a entrada das entradas padrão, assim como com o head. Tente executar este comando:

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

Você verá o resultado:

Output
fish lobsters sharks

O comportamento das funções flag que você viu até agora tem sido limitado a examinar a invocação do comando inteiro. Nem sempre você vai desejar esse comportamento, especialmente se estiver escrevendo uma ferramenta de linha de comando que suporte subcomandos.

Usando o flagSet para implementar subcomandos

Os aplicativos de linha de comando modernos com frequência implementam “subcomandos” para agrupar um conjunto de ferramentas sob um único comando. A ferramenta mais conhecida que usa esse padrão é a git. Ao examinar um comando como o git init, o git é o comando e o init é o subcomando do git. Uma característica notável dos subcomandos é que cada subcomando pode ter sua própria coleção de sinalizadores.

Os aplicativos em Go podem ser oferecer suporte aos subcomandos com seu próprio conjunto de sinalizadores, usando o tipo flag.( *FlagSet). Para demonstrar isso, crie um programa que implementa um comando usando dois subcomandos com sinalizadores diferentes.

Crie um novo arquivo chamado subcommand.go e adicione o seguinte conteúdo ao arquivo:

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

Esse programa está dividido em algumas partes: a função main, a função root e as funções individuais para implementar o subcomando. A função main manipula os erros retornados dos comandos. Caso alguma função retorne um erro, a instrução if irá capturá-lo, imprimi-lo e o programa fechará com um código de status de 1, indicando ao resto do sistema operacional que um erro ocorreu. Dentro do main, enviamos todos os argumentos com os quais o programa foi invocado para o root. Removemos o primeiro argumento, que é o nome do programa (nos exemplos anteriores ./subcommand) dividindo o os.Args primeiro.

A função root define o []Runner, onde todos os subcomandos seriam definidos. Runner é uma interface para subcomandos que permite à root recuperar o nome do subcomando usando o Name() e compará-lo ao conteúdo da variável subcommand. Assim que o subcomando correto for localizado, após a iteração através da variável cmds, inicializamos o subcomando com o resto dos argumentos e invocamos o método Run () do comando.

Definimos apenas um subcomando, embora esse framework nos permita facilmente criar outros. O GreetCommand é instanciado usando o NewGreetCommand, onde criamos um novo *flag.FlagSet usando o flag.NewFlagSet. O flag.NewFlagSet recebe dois argumentos: um nome para o conjunto de sinalizadores e uma estratégia para a comunicação dos erros de análise. O nome do *flag.FlagSet é acessível usando o método flag.( *FlagSet). Name. Usamos isso no método (*GreetCommand). Name() para que o nome do subcomando corresponda ao nome que demos ao *flag.FlagSet. O NewGreetCommand também define um sinalizador -name de maneira semelhando à dos exemplos anteriores, mas ele chama isso como um método fora do campo *flag.FlagSet do *GreetCommand, gc.fs. Quando a root chama o método Init() do *GreetCommand, enviamos os argumentos fornecidos para o método Parse do campo *flag.FlagSet.

Será mais fácil ver os subcomandos se você compilar este programa e, em seguida, executá-lo. Compile o programa:

  • go build subcommand.go

Agora, execute o programa sem argumentos:

  • ./subcommand

Você verá este resultado:

Output
You must pass a sub-command

Agora, execute o programa com o subcomando greet:

  • ./subcommand greet

Isso produz o seguinte resultado:

Output
Hello World !

Agora, utilize o sinalizador -name com o greet para especificar um nome:

  • ./subcommand greet -name Sammy

Você verá este resultado do programa:

Output
Hello Sammy !

Esse exemplo ilustra alguns princípios por trás do fato de como aplicativos de linha de comando maiores poderiam ser estruturados em Go. Os FlagSets foram desenvolvidos para dar aos desenvolvedores mais controle sobre onde e como os sinalizadores são processadas pela lógica de análise de sinalizadores.

Conclusão

Os sinalizadores tornam seus aplicativos mais úteis em mais contextos porque dão aos usuários controle sobre como os programas são executados. É importante dar aos usuários padrões úteis, mas você também deve dar a eles a oportunidade de substituir as configurações que não funcionam para sua situação. Você viu que o pacote flag oferece escolhas flexíveis para apresentar opções de configuração aos seus usuários. Você pode escolher alguns sinalizadores simples ou compilar um conjunto expansível de subcomandos. Em qualquer caso, usar o pacote flag ajudará na compilação de utilitários no estilo da longa história de ferramentas de linha de comando flexíveis e com scripts.

Para aprender mais sobre a linguagem de programação Go, confira nossa série completa de artigos sobre Como programar em Go.

Creative Commons License