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.
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:
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.Printl
n quando o sinalizador estiver ausente.
Salve o arquivo e execute o programa sem nenhum sinalizador:
- go run boolean.go
Você verá o seguinte resultado:
OutputHello, 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.
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:
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:
Outputpackage 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:
Outputpackage 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:
Outputfish
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.
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:
OutputYou must pass a sub-command
Agora, execute o programa com o subcomando greet
:
- ./subcommand greet
Isso produz o seguinte resultado:
OutputHello World !
Agora, utilize o sinalizador -name
com o greet
para especificar um nome:
- ./subcommand greet -name Sammy
Você verá este resultado do programa:
OutputHello 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.
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!