Vamos Lá: Programas de Linha de Comando com Golang
() translation by (you can also view the original English article)
Visão Geral
A linguagem Go é uma nova e excitante linguagem que tem grande popularidade por um bom motivo. Nesse tutorial, aprenderemos como escrever programas de linha de comando com Go. O programa exemplo, multi-git, permitirá executar comandos do git em vários repositórios ao mesmo tempo.
Breve Introdução à Go
Go é uma linguagem de código aberto ao estilo C, criada no Google por alguns dos hackers que criaram C e Unix, por conta do desgoto pelo C++. Isso se vê no projeto, com decisões ortodóxicas, como evitar a implementação de herança, modelos (templates) e exceções. Go é simples, confiável e eficiente. Sua característica mais distinta é seu suporte à programação concorrente através das goroutines e canais (channels).
Antes de dissecarmos o programa exemplo, siga o guia oficial para estar hapto ao desenvolvimento com Go.
O Programa Multi-Git
O multi-git é um programa simples mas útil, feito em Go. Se trabalhamos em uma equipe em que o código é dividido em muitos repositórios git, com frequência, precisamos realizar mudanças em vários deles. Isso é um problema porque o git não possui conceito de múltiplos repositórios. Tudo gira em torno de um único repositório.
Isso vira um problema se usamos ramificações (branches). Se uma funcionalidade envolve três repositórios, precisaremos de uma ramificação em cada um deles e teremos de lembrar de realizar check out, pull, push e merge em todos. Isso não é simples. Multi-git administra um conjunto de repositórios e permite você opera sobre todo o conjunto de uma só vez. Note que a versão atual do multi-git requer a criação manual das ramificações, mas adicionarei essa funcionalidade futuramente.
Ao explorarmos a implementação do multi-git, aprenderemos muito sobre a escrita de programas de linha de comando em GO.
Pacotes e Importação
Programas em Go são organizados em pacotes. O multi-git consiste em um único arquivo chamado main.go. No topo, especificamos o pacote 'main', seguido por uma lista de importações: Outros pacotes que usaremos no multi-git.
1 |
package main |
2 |
|
3 |
|
4 |
|
5 |
import ( |
6 |
|
7 |
"flag"
|
8 |
|
9 |
"fmt"
|
10 |
|
11 |
"log"
|
12 |
|
13 |
"os"
|
14 |
|
15 |
"strings"
|
16 |
|
17 |
"os/exec"
|
18 |
|
19 |
)
|
Por exemplo, o fmt é usado para formatar Entrada e Saída (I/O) parecido com os prinf e scanf do C. Go suporta a instalação de pacotes de vários lugares, através do comando go get
. Quando instalamos pacotes, eles estarão em um namespace sob a variável de ambiente $GOPATH. Podemos instalar pacotes de lugares como o GitHub, Bitbucket, Google code, Launchpad e até dos servicos DevOps da IBM, através de vários formatos de vesionamento, como git, subversion, mercurial e bazaar.
Argumentos da Linha de Comando.
Argumentos da linha de comando são uma das formas mais comuns de prover entradas para programas. Eles são fáceis de usar, permitem-nos executar e configurar programas em um linha, e tem ótimo suporte a análise em várias linguagens. Go as chama de "flags" de linha-de-comando e tem o pacote flag para especificá-las e analisá-las.
Tipicamente, analisamos os argumentos de linha de comando no começo do programs e o multi-git segue essa convenção. O ponto de entrada é a função main()
. As primeiras duas linhas definem os argumentos "command" e "ignoreErrors". Cada argumento tem um nome, tipo de dado, valor padrão e texto de ajuda. A chamada flag.Parse()
analisará o que foi passado para o programa e povoará as flags definidas.
1 |
func main() { |
2 |
|
3 |
command := flag.String("command", "", "The git command") |
4 |
|
5 |
ignoreErrors := flag.Bool( |
6 |
|
7 |
"ignore-errors", |
8 |
|
9 |
false, |
10 |
|
11 |
"Keep running after error if true") |
12 |
|
13 |
flag.Parse() |
Também é possível acessar argumentos não definidos através da função flag.Args()
. Assim, as flags são os argumentos pré-definidos e "args" são os argumentos não processados. Os argumentos não processados são indexados a partir do 0.
Variáveis de Ambiente
Outra forma de configuração de programas são as variáveis de ambiente. Quando usamos variáveis de ambiente, podemos executar o mesmo programa várias vezes no mesmo ambiente e, todas as vezes, elas serão usadas.
Multi-git usa duas dessas variáveis: "MG_ROOT" e "MG_REPOS". Ele foi projetado para administrar um grupo de repositórios git que possuem um diretório pai comum. Esse é o "MG_ROOT". Os nomes dos repositórios estão na "MG_REPOS", como uma cadeia de caracteres separadas por vírgulas. Para ler seus valores, podemos usar a função os.Getenv()
.
1 |
// Get managed repos from environment variables
|
2 |
|
3 |
root := os.Getenv("MG_ROOT") |
4 |
|
5 |
if root[len(root) - 1] != '/' { |
6 |
|
7 |
root += "/" |
8 |
|
9 |
}
|
10 |
|
11 |
|
12 |
|
13 |
repo_names := strings.Split(os.Getenv("MG_REPOS"), ",") |
Verificando a Lista de Repositórios
Encontrado o diretório raiz e os nomes de todos os repositórios, verificamos que os repositórios estão presentes e que são, realmente, repositórios git. A verificação é a buscar por um sub-diretório .git em cada um dos diretórios dos repositórios.
Primeiro, definimos um vetor de cadeia de caracteres chamado "repos". Então, iteramos por todos os nomes de repositórios e construímos seus caminhos, concatenando seus nomes ao caminho do diretório raiz. Se a chamada [os.Stat()]()
falhar para o subdiretório .git, ele registra o erro e sai. Caso contrário, o caminho é anexado ao vetor dos repositórios.
1 |
var repos []string |
2 |
|
3 |
// Verify all repos exist and are actually git repos (have .git sub-dir)
|
4 |
|
5 |
for _, r := range repo_names { |
6 |
|
7 |
path := root + r |
8 |
|
9 |
_, err := os.Stat(path + "/.git") |
10 |
|
11 |
if err != nil { |
12 |
|
13 |
log.Fatal(err) |
14 |
|
15 |
}
|
16 |
|
17 |
repos = append(repos, path) |
18 |
|
19 |
}
|
Go tem um mecanismo de manipulação de erros único, onde funções retornam tanto valores e objetos de erro. Veja como os.Stat()
retorna dois valores. Nesse caso, o espaço reservado "_" é usado para guardar o valor atual porque você apenas se importará com o erro. Go é restrita e requer que variáveis nomeadas sejam usadas. Se não planeja usar um valor, atribua-o a "_" para evitar erros de compilação.
Executando Comandos de Terminal
Agora, já temos nossa lista de caminhos repositórios onde queremos executar os comandos git. Talvez lembre que recebemos o comando git como uma única flag chamada "command". Precisamos dividi-la em um vetor (com comando git, sub-comando e opções). O comando inteiro como cadeia de caracteres também é armazenado para posterior visualização.
1 |
// Break the git command into components (needed to execute)
|
2 |
|
3 |
var git_components []string |
4 |
|
5 |
for _, component := range strings.Split(*command, " ") { |
6 |
|
7 |
git_components = append(git_components, component) |
8 |
|
9 |
}
|
10 |
|
11 |
command_string := "git " + *command |
Agora, estamos prontos para iterar por cada repositório e executar o comando git em cada um. O laço "for ... range" será usado novamente. Primeiro, o multi-git altera o diretório de execução para o repositório alvo "r" e imprime o comando git. Então executa o comando, usando a função exec.Command()
e imprime o retorno combinado (tanto retorno padrão quanto erro padrão).
Por fim, ele verifica se houve erro durante a execução. Se houve erro e a flag ignoreErrors
for falso, então multi-git para. O motivo para, opcionalmente, ignorar erros é que, algumas vezes, é OK caso um comando falhe em algum repositório. Por exemplo, se quiser verificar uma ramificação chamada "cool feature" em todos os repositórios que tem essa ramificação, e não se importa se o checkout falhar em algum repositório que não a tiver.
1 |
for _, r := range repos { |
2 |
|
3 |
// Go to the repo's directory
|
4 |
|
5 |
os.Chdir(r); |
6 |
|
7 |
|
8 |
|
9 |
// Print the command
|
10 |
|
11 |
fmt.Printf("[%s] %s\n", r, command_string) |
12 |
|
13 |
|
14 |
|
15 |
// Execute the command
|
16 |
|
17 |
out, err := exec.Command("git", git_components...).CombinedOutput() |
18 |
|
19 |
|
20 |
|
21 |
// Print the result
|
22 |
|
23 |
fmt.Println(string(out)) |
24 |
|
25 |
|
26 |
|
27 |
// Bail out if there was an error and NOT ignoring errors
|
28 |
|
29 |
if err != nil && !*ignoreErrors { |
30 |
|
31 |
os.Exit(1) |
32 |
|
33 |
}
|
34 |
|
35 |
}
|
36 |
|
37 |
|
38 |
|
39 |
fmt.Println("Done.") |
Conclusão
Go é simples porém poderosa. É projetada para programação de sistemas larga escala, mas funciona bem para pequenos programas de linhas de comando. O minimalismo da Go, vai de encontro às outras linguagens modernas, como Scala e Rust (também poderosas e bem projetadas), mas com curva acentuada de aprendizado. Torço para que tente Go. É divertido.