Hoje vamos ver todo o poder quem uma CLI (Command line interface) pode trazer para o desenvolvimento, uma CLI pode nos ajudar a executar tarefas de forma mais eficaz e leve através de comandos via terminal, sem precisar de uma interface. Como por exemplo o git e o Docker , praticamente usamos a CLI deles o tempo inteiro, quando executamos um git commit -m "commit message" ou docker ps -a estamos utilizando uma CLI. Vou deixar um artigo que detalha melhor o que é uma CLI.
Nesse post vamos criar um boilerplate para projetos em GO, onde com apenas 1 comando via CLI, vai ser criado toda a estrutura do projeto.
GO e CLI
Bom, o Go é extremamente poderoso para contrução de CLI, é umas das linguagens mais utilizadas para isso, não é atoa que é a amplamente utilizada entre os DevOps, justamente por ser tão poderosa e simples.
Só para dar um exemplo do poder do Go para construções de CLI, você já deve ter utilizado ou pelo menos ouviu falar do Docker, Kubernetes, Prometheus, Terraform,mas o que todos eles tem em comum? todos eles tem grande parte da sua usabilidade via CLI e são desenvolvidos em Go 🐿.
Iniciando uma CLI com GO
O Go tem um pacote para lidar com CLI de forma nativa. Mas vamos abordar de forma rápida, o intuito do post é utilizar o pacote Cobra CLI, que vai facilitar a construção da nossa CLI.
Vamos utilizar o pacote flag
package main
import (
"flag"
"fmt"
"time"
)
func main() {
dateFlag := flag.Bool("date", false, "Exibir a data atual")
flag.Parse()
if *dateFlag {
currentTime := time.Now()
fmt.Println("Data atual:", currentTime.Format("2006-01-02 15:04:05"))
}
}
Nesse exemplo acima, criamos uma flag date, ao passar essa flag é retornado a data atual, algo bem simples, rodando o projeto com go run main.go --date, vamos ter o valor Data atual: 2023-11-15 12:26:14.
dateFlag := flag.Bool("date", false, "Exibir a data atual")
No código acima, criamos uma flag, o primeiro argumento date é o nome de flag, o false como valor padrão significa que, se você rodar o programa sem especificar explicitamente a flag --date, o valor associado a dateFlag será false. Isso permite que o programa tenha um comportamento padrão específico caso essa flag não seja fornecida quando o programa é executado, já o terceiro argumento Exibir a data atual é o detalhe do que essa flag faz.
Se rodarmos:
go run main.go -h
Recebemos:
-date
Exibir a data atual
Podemos usar a flag com --date ou -date, o Go já faz a verificação automática.
Podemos fazer todo o nosso boilerplate com essa abordage, porém vamos facilitar um pouco e usar o pacote Cobra CLI.
Cobra CLI
Esse pacote é muito utilizado para contruções de CLI poderosas, é utilizado por exemplo para o Kubernetes CLI e GitHub CLI, além de oferecer alguns recursos bacanas como preenchimento automático do shell, reconhecimento automático de sinalizadores (as tags), podendo utilizar -h ou -help por exemplo, entre outras facilidades.
Criando o projeto
Nosso projeto vai ser bem simples, vamos ter apenas o main.go e o go.mod e consequentemente nosso go.sum, vamos iniciar o projeto com o comando:
go mod init github.com/wiliamvj/boilerplate-cli-go
Você pode utilizar o nome que desejar, por convenção geralmente criamos o nome do projeto sendo o link do nosso repositório.
ficando assim:
Agora vamos baixar o pacote Cobra com o comando:
go get -u github.com/spf13/cobra@latest
Nosso boilerplate vai ter uma estrutura bem simples, a ideia é criar uma estrutura muito utilizada pela comunidade em Go, veja como vai ficar:
-
cmd: Aqui é onde vamos deixar o
main.goque inicia nosso app. -
internal: Nessa pasta onde deve ficar todo o código da nossa aplicação.
- handler: Aqui vai ficar os arquivos responsáveis por receber nossas solicitações http, você pode conhecer também como controllers.
- routes: Aqui vamos organizar nossas rotas.
Não é a estrutura completa, estamos apenas criando o básico para o nosso exemplo.
Todo o nosso código vai se concentrar em nosso main.go.
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
func main() {
var rootCommand = &cobra.Command{}
var projectName, projectPath string
var cmd = &cobra.Command{
Use: "create",
Short: "Create boilerplate for a new project",
Run: func(cmd *cobra.Command, args []string) {
// validations
if projectName == "" {
fmt.Println("You must supply a project name.")
return
}
if projectPath == "" {
fmt.Println("You must supply a project path.")
return
}
fmt.Println("Creating project...")
},
}
cmd.Flags().StringVarP(&projectName, "name", "n", "", "Name of the project")
cmd.Flags().StringVarP(&projectPath, "path", "p", "", "Path where the project will be created")
rootCommand.AddCommand(cmd)
rootCommand.Execute()
}
O código acima é apenas para iniciar o nosso CLI, vamos ter apenas duas váriaveis:
-
projectName: será o nome do nosso projeto que vamos capturar no input da nossa CLI. -
projectPath: será o caminho onde o boilerplate será criado, vamos capturar no input do CLI. -
&cobra.Command{}: inicia o pacote Cobra. -
Run:Recebe uma funcão anônima, é nessa função que capturamos o input do usuário digitado no CLI e validamos, nossa validação é simples, apenas verificamos se oprojectNamee oprojectPathnão são nulos. -
cmd.Flags(): Aqui criamos as flags com sinalizadores, dessa forma pode ser usado-nameou-n, ambos serão aceitos, também colocamos a descrição do que esse sinalizado faz. -
rootCommand.AddCommand(cmd): Adicionamos nossocmdaorootCommandcriado no inicio do nossomain.go. -
rootCommand.Execute(): Por fim, executamos nossa CLI.
Isso é tudo que precisamos para deixar nossa CLI funcionando, claro que sem a lógica do nosso boilerplate, mas com isso já conseguimos utilizar via terminal. Vamos testar!
Podemos fazer um build do projeto o usar sem o build
Com build:
go build -o cli .
Vai criar na raiz um arquivo chamado cli, vamos rodar o binário da nossa CLI:
./cli --help
Vamos ter uma saida igual a essa:
Usage:
[command]
Available Commands:
completion Generate the autocompletion script for the specified shell
create Create boilerplate for a new project
help Help about any command
Flags:
-h, --help help for this command
Use " [command] --help" for more information about a command.
Veja que já temos as dicas de como utilizar o comando que criamos create Create boilerplate for a new project, se rodarmos:
./cli create --help
Teremos:
Create boilerplate for a new project
Usage:
create [flags]
Flags:
-h, --help help for create
-n, --name string Name of the project
-p, --path string Path where the project will be created
Vamos rodar agora passando nossas flags:
./cli create -n my-project -p ~/documents
Vamos ter nossa mensagem Creating project..., indicando que funcionou, mas nada ainda acontece, pois não implementamos a lógica.
Podemos ainda criar subcomandos, novas flags, novas validações, mas por enquanto vamos deixar assim, se quiser você pode criar mais opções, veja a documentação do pacote Cobra.
Criando o boilerplate
Com a nossa CLI pronta, vamos agora a lógica do boilerplate, que é bem simples, teremos que criar as pastas, depois precisamos criar os arquivos e por fim abrir os arquivos e inserir o código, para isso vamos utilizar bastante o pacote os do Go, que permite acessar recursos do sistema operacional.
Vamos primeiro pegar o diretório principal e validar se já existe uma pasta com o nome que vai se usado para criar o nosso projeto:
globalPath := filepath.Join(projectPath, projectName)
if _, err := os.Stat(globalPath); err == nil {
fmt.Println("Project directory already exists.")
return
}
Se passarmos o projectName como test e o projectPath como /documents, isso valida se não existe nenhuma outra pasta em documents chamado test, se existir returnamos e devolvemos uma mensagem de erro.
Você pode modificar e caso exista uma pasta com mesmo nome, alterar o nome do projectName ou deletar a pasta que já existe, mas por hora vamos apenas retornar erro.
if err := os.Mkdir(globalPath, os.ModePerm); err != nil {
log.Fatal(err)
}
Nessa parte, vamos criar o diretório no caminho que foi informado usando nossa flag -p, se usarmos:
./cli create -n my-project -p ~/documents
Iniciando o Go
Vai ser criado uma pasta chamada my-project no diretório documents.
startGo := exec.Command("go", "mod", "init", projectName)
startGo.Dir = globalPath
startGo.Stdout = os.Stdout
startGo.Stderr = os.Stderr
err := startGo.Run()
if err != nil {
log.Fatal(err)
}
No código acima executamos o comando para iniciar o projeto em Go, vai ser criado no diretório raiz que escolhemos, no nosso exemplo vai rodar dentro de documents/my-project, isso vai criar o arquivo go.mod e vai definir o nome do módulo como my-projects.
-
exec.Command: Cria o comando que vamos rodar no terminal, no caso vai sergo mod init my-project. -
startGo.Dir: Determinar onde vai rodar esse comando, no exemplo vai rodar em documents/my-project. -
startGo.Stdout: Vai colocar no terminal o retorno do comando, vai retornargo: creating new go.mod: module my-project. -
startGo.Stderr: Redireciona a saida de um possivel erro para onde o programa está sendo executado. -
startGo.Run(): Por fim, executamos tudo.
Criando as pastas
Vamos criar nossas pastas, são elas cmd, internal, handler e routes.
cmdPath := filepath.Join(globalPath, "cmd")
if err := os.Mkdir(cmdPath, os.ModePerm); err != nil {
log.Fatal(err)
}
internalPath := filepath.Join(globalPath, "internal")
if err := os.Mkdir(internalPath, os.ModePerm); err != nil {
log.Fatal(err)
}
handlerPath := filepath.Join(internalPath, "handler")
if err := os.Mkdir(handlerPath, os.ModePerm); err != nil {
log.Fatal(err)
}
routesPath := filepath.Join(handlerPath, "routes")
if err := os.Mkdir(routesPath, os.ModePerm); err != nil {
log.Fatal(err)
}
Esse código acima cria na sequência as pastas necessárias, usando os.Mkdir, (veja nas docs), para as pastas handler e routes, precisamos acessar a pasta internal, pois serão criadas dentro da internal, para isso pegamos usando o Join mesclamos o caminho, ficando:
-
handlerPath: documents/my-project/internal -
routesPath: documents/my-project/internal/handler
Criando os arquivos
Com as pastas criadas, vamos criar os aquivos, para exemplo vamos criar o main.go é claro e o routes.go, dentro da pasta routes.
mainPath := filepath.Join(cmdPath, "main.go")
mainFile, err := os.Create(mainPath)
if err != nil {
log.Fatal(err)
}
defer mainFile.Close()
routesFilePath := filepath.Join(routesPath, "routes.go")
routesFile, err := os.Create(routesFilePath)
if err != nil {
log.Fatal(err)
}
defer routesFile.Close()
Acima criamos os arquivos main.go e routes.go.
-
mainPath: determinamos o caminho, usando omainPathusado para criar a pasta cmd. -
os.Create(mainPath): Criamos o arquivo, no diretório especificado. (documents/my-project/cmd) -
routesFilePath: determinamos o caminho, usando oroutesPathusado para criar a pasta routes. -
os.Create(routesFilePath): Criamos o arquivo, no diretório especificado. (documents/my-project/internal/handler/routes) -
defer routesFile.Close(): Fechamos o arquivo,defer, usando essa palavra reservada do GO, garantimos que a última coisa a acontecer é fechar o arquivo. Veja mais sobre odeferaqui.
Escrevendo nos arquivos
Com as pastas e arquivos criados, agora vamos escrever nos arquivos main.go e routes.go, vamos fazer algo simples, apenas para exemplo, para organizar melhor, vamos separar em funções que escrevem em cada arquivo.
func WriteMainFile(mainPath string) error {
packageContent := []byte(`package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
`)
mainFile, err := os.OpenFile(mainPath, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}
defer mainFile.Close()
_, err = mainFile.Write(packageContent)
if err != nil {
return err
}
return nil
}
Na função acima, recemos por parâmetro o mainPath, que é o caminho do arquivo, vamos adiciona um código simples, que apenas fazer o log de um Hello World.
-
packageContent: Criamos o código que vai ser escrito no arquivo. -
os.OpenFile: Abrimos o arquivo especificado emmainPath. -
defer mainFile.Close(): Fechamos o arquivo por último comdefer. -
mainFile.Write: Por fim, escrevemos no arquivo, e tratamos o erro se houver.
O_WRONLY e O_APPEND, são constantes usadas para definir modo de abertura de um arquivo, O_WRONLY indica que o arquivo será aberto apenas para escrita, O_APPENDisso faz com o conteúdo adicionado serão acrescentados no fim do arquivo, sem sobrescrever o conteúdo existente.
func WriteRoutesFile(routesFilePath string) error {
packageContent := []byte(`package routes
// Seu código aqui
`)
routesFile, err := os.OpenFile(routesFilePath, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}
defer routesFile.Close()
_, err = routesFile.Write(packageContent)
if err != nil {
return err
}
return nil
}
Fazemos o mesmo para o arquivo routes.go.
Agora basta chamar as novas funções na função main, ficando assim:
mainPath := filepath.Join(cmdPath, "main.go")
mainFile, err := os.Create(mainPath)
if err != nil {
log.Fatal(err)
}
defer mainFile.Close()
if err := WriteMainFile(mainPath); err != nil {
log.Fatal(err)
}
routesFilePath := filepath.Join(routesPath, "routes.go")
routesFile, err := os.Create(routesFilePath)
if err != nil {
log.Fatal(err)
}
defer routesFile.Close()
if err := WriteRoutesFile(routesFilePath); err != nil {
log.Fatal(err)
}
Código final
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"github.com/spf13/cobra"
)
func main() {
var rootCommand = &cobra.Command{}
var projectName, projectPath string
var cmd = &cobra.Command{
Use: "create",
Short: "Create boilerplate for a new project",
Run: func(cmd *cobra.Command, args []string) {
if projectName == "" {
fmt.Println("You must supply a project name.")
return
}
if projectPath == "" {
fmt.Println("You must supply a project path.")
return
}
fmt.Println("Creating project...")
globalPath := filepath.Join(projectPath, projectName)
if _, err := os.Stat(globalPath); err == nil {
fmt.Println("Project directory already exists.")
return
}
if err := os.Mkdir(globalPath, os.ModePerm); err != nil {
log.Fatal(err)
}
startGo := exec.Command("go", "mod", "init", projectName)
startGo.Dir = globalPath
startGo.Stdout = os.Stdout
startGo.Stderr = os.Stderr
err := startGo.Run()
if err != nil {
log.Fatal(err)
}
cmdPath := filepath.Join(globalPath, "cmd")
if err := os.Mkdir(cmdPath, os.ModePerm); err != nil {
log.Fatal(err)
}
internalPath := filepath.Join(globalPath, "internal")
if err := os.Mkdir(internalPath, os.ModePerm); err != nil {
log.Fatal(err)
}
handlerPath := filepath.Join(internalPath, "handler")
if err := os.Mkdir(handlerPath, os.ModePerm); err != nil {
log.Fatal(err)
}
routesPath := filepath.Join(handlerPath, "routes")
fmt.Println(routesPath)
if err := os.Mkdir(routesPath, os.ModePerm); err != nil {
log.Fatal(err)
}
mainPath := filepath.Join(cmdPath, "main.go")
mainFile, err := os.Create(mainPath)
if err != nil {
log.Fatal(err)
}
defer mainFile.Close()
if err := WriteMainFile(mainPath); err != nil {
log.Fatal(err)
}
routesFilePath := filepath.Join(routesPath, "routes.go")
routesFile, err := os.Create(routesFilePath)
if err != nil {
log.Fatal(err)
}
defer routesFile.Close()
if err := WriteRoutesFile(routesFilePath); err != nil {
log.Fatal(err)
}
},
}
cmd.Flags().StringVarP(&projectName, "name", "n", "", "Name of the project")
cmd.Flags().StringVarP(&projectPath, "path", "p", "", "Path where the project will be created")
rootCommand.AddCommand(cmd)
rootCommand.Execute()
}
func WriteMainFile(mainPath string) error {
packageContent := []byte(`package main
import "fmt"
func main() {
fmt.Println("Hello World!")
}
`)
mainFile, err := os.OpenFile(mainPath, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}
defer mainFile.Close()
_, err = mainFile.Write(packageContent)
if err != nil {
return err
}
return nil
}
func WriteRoutesFile(routesFilePath string) error {
packageContent := []byte(`package routes
// Seu código aqui
`)
routesFile, err := os.OpenFile(routesFilePath, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
return err
}
defer routesFile.Close()
_, err = routesFile.Write(packageContent)
if err != nil {
return err
}
return nil
}
Testando a CLI
Bom, com tudo pronto, vamos testar! Para isso vamos compilar nosso código com o bom e velho go build.
go build -o cli .
Executando a CLI:
./cli create -n my-project -p ~/documents
Vamos ter o retorno:
Creating project...
go: creating new go.mod: module my-project
Acessando nosso projeto e abrindo no Visual Studio Code com:
cd /documents/my-project && code .
Teremos noss boilerplate criado:
Se rodarmos o projeto criado via CLI, podemos ver que tudo funciona.
go run cmd/main.go
output:
Hello World!
Com isso finalizamos a criação de nossa CLI que cria um boilerplate.
Considerações finais
Vimos o poder que uma CLI pode nos proporcionar, sem contar a rapidez da sua execução. Usando o pacote Cobra CLI temos ainda mais facilidade, a criação de um boilerplate é apenas um exemplo, podemos automatizar muitas tarefas.
O nosso boilerplate poderia ser ainda mais automatizado, conseguimos por exemplo instalar um pacote como o Go Chi, criando endpoints padrões, tudo isso usando a CLI, você pode até mesmo criar seu próprio framework, já pensou que com apenas 1 comando seu projeto inicial já vem todo configurado?
Com o conhecimento em na criação de CLI, você tem um grande poder em suas mãos!
Link do repositório
repositório do projeto
link do projeto no meu blog



Top comments (0)