Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?

O poder do CLI com Golang e Cobra CLI

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 through 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 put up vamos criar um boilerplate para projetos em GO, onde com apenas 1 comando through 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 through 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 put up é utilizar o pacote Cobra CLI, que vai facilitar a construção da nossa CLI.

Vamos utilizar o pacote flag

  package deal fundamental

  import (
    "flag"
    "fmt"
    "time"
  )

  func fundamental() {
    dateFlag := flag.Bool("date", false, "Exibir a knowledge atual")
    flag.Parse()

    if *dateFlag {
      currentTime := time.Now()
      fmt.Println("Knowledge atual:", currentTime.Format("2006-01-02 15:04:05"))
    }
  }
Enter fullscreen mode

Exit fullscreen mode

Nesse exemplo acima, criamos uma flag date, ao passar essa flag é retornado a knowledge atual, algo bem simples, rodando o projeto com go run fundamental.go --date, vamos ter o valor Knowledge atual: 2023-11-15 12:26:14.

  dateFlag := flag.Bool("date", false, "Exibir a knowledge atual")
Enter fullscreen mode

Exit fullscreen mode

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 knowledge atual é o detalhe do que essa flag faz.

Se rodarmos:

  go run fundamental.go -h
Enter fullscreen mode

Exit fullscreen mode

Recebemos:

  -date
    Exibir a knowledge atual
Enter fullscreen mode

Exit fullscreen mode

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 fundamental.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
Enter fullscreen mode

Exit fullscreen mode

Você pode utilizar o nome que desejar, por convenção geralmente criamos o nome do projeto sendo o hyperlink do nosso repositório.

ficando assim:

Agora vamos baixar o pacote Cobra com o comando:

  go get -u github.com/spf13/cobra@newest
Enter fullscreen mode

Exit fullscreen mode

Nosso boilerplate vai ter uma estrutura bem simples, a ideia é criar uma estrutura muito utilizada pela comunidade em Go, veja como vai ficar:

Project structure

  • cmd: Aqui é onde vamos deixar o fundamental.go que inicia nosso app.
  • inside: 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 fundamental.go.

  package deal fundamental

  import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
  )

  func fundamental() {
    var rootCommand = &cobra.Command{}
    var projectName, projectPath string

    var cmd = &cobra.Command{
      Use:   "create",
      Brief: "Create boilerplate for a brand new challenge",
      Run: func(cmd *cobra.Command, args []string) {
        // validations
        if projectName == "" {
          fmt.Println("You need to provide a challenge identify.")
          return
        }
        if projectPath == "" {
          fmt.Println("You need to provide a challenge path.")
          return
        }
        fmt.Println("Creating challenge...")
      },
    }

    cmd.Flags().StringVarP(&projectName, "identify", "n", "", "Title of the challenge")
    cmd.Flags().StringVarP(&projectPath, "path", "p", "", "Path the place the challenge will probably be created")

    rootCommand.AddCommand(cmd)
    rootCommand.Execute()
  }
Enter fullscreen mode

Exit fullscreen mode

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 enter da nossa CLI.
  • projectPath: será o caminho onde o boilerplate será criado, vamos capturar no enter do CLI.
  • &cobra.Command{}: inicia o pacote Cobra.
  • Run: Recebe uma funcão anônima, é nessa função que capturamos o enter do usuário digitado no CLI e validamos, nossa validação é simples, apenas verificamos se o projectName e o projectPath não são nulos.
  • cmd.Flags(): Aqui criamos as flags com sinalizadores, dessa forma pode ser usado -name ou -n, ambos serão aceitos, também colocamos a descrição do que esse sinalizado faz.
  • rootCommand.AddCommand(cmd): Adicionamos nosso cmd ao rootCommand criado no inicio do nosso fundamental.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 through terminal. Vamos testar!

Podemos fazer um construct do projeto o usar sem o construct

Com construct:

  go construct -o cli .
Enter fullscreen mode

Exit fullscreen mode

Vai criar na raiz um arquivo chamado cli, vamos rodar o binário da nossa CLI:

  ./cli --help
Enter fullscreen mode

Exit fullscreen mode

Vamos ter uma saida igual a essa:

  Utilization:
    [command]

  Out there Instructions:
    completion  Generate the autocompletion script for the required shell
    create      Create boilerplate for a brand new challenge
    assist        Assist about any command

  Flags:
    -h, --help   assist for this command

  Use " [command] --help" for extra details about a command.
Enter fullscreen mode

Exit fullscreen mode

Veja que já temos as dicas de como utilizar o comando que criamos create Create boilerplate for a brand new challenge, se rodarmos:

  ./cli create --help
Enter fullscreen mode

Exit fullscreen mode

Teremos:

  Create boilerplate for a brand new challenge

  Utilization:
    create [flags]

  Flags:
    -h, --help          assist for create
    -n, --name string   Title of the challenge
    -p, --path string   Path the place the challenge will probably be created
Enter fullscreen mode

Exit fullscreen mode

Vamos rodar agora passando nossas flags:

  ./cli create -n my-project -p ~/paperwork
Enter fullscreen mode

Exit fullscreen mode

Vamos ter nossa mensagem Creating challenge..., 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.Be part of(projectPath, projectName)

  if _, err := os.Stat(globalPath); err == nil {
    fmt.Println("Undertaking listing already exists.")
    return
  }
Enter fullscreen mode

Exit fullscreen mode

Se passarmos o projectName como check e o projectPath como /paperwork, isso valida se não existe nenhuma outra pasta em paperwork chamado check, 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.Deadly(err)
  }
Enter fullscreen mode

Exit fullscreen mode

Nessa parte, vamos criar o diretório no caminho que foi informado usando nossa flag -p, se usarmos:

  ./cli create -n my-project -p ~/paperwork
Enter fullscreen mode

Exit fullscreen mode



Iniciando o Go

Vai ser criado uma pasta chamada my-project no diretório paperwork.

  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.Deadly(err)
  }
Enter fullscreen mode

Exit fullscreen mode

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 paperwork/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 ser go mod init my-project.
  • startGo.Dir: Determinar onde vai rodar esse comando, no exemplo vai rodar em paperwork/my-project.
  • startGo.Stdout: Vai colocar no terminal o retorno do comando, vai retornar go: 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, inside, handler e routes.

  cmdPath := filepath.Be part of(globalPath, "cmd")
    if err := os.Mkdir(cmdPath, os.ModePerm); err != nil {
    log.Deadly(err)
  }
  internalPath := filepath.Be part of(globalPath, "inside")
  if err := os.Mkdir(internalPath, os.ModePerm); err != nil {
    log.Deadly(err)
  }
  handlerPath := filepath.Be part of(internalPath, "handler")
    if err := os.Mkdir(handlerPath, os.ModePerm); err != nil {
    log.Deadly(err)
  }
  routesPath := filepath.Be part of(handlerPath, "routes")
    if err := os.Mkdir(routesPath, os.ModePerm); err != nil {
    log.Deadly(err)
  }
Enter fullscreen mode

Exit fullscreen mode

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 inside, pois serão criadas dentro da inside, para isso pegamos usando o Be part of mesclamos o caminho, ficando:

  • handlerPath: paperwork/my-project/inside
  • routesPath: paperwork/my-project/inside/handler



Criando os arquivos

Com as pastas criadas, vamos criar os aquivos, para exemplo vamos criar o fundamental.go é claro e o routes.go, dentro da pasta routes.

  mainPath := filepath.Be part of(cmdPath, "fundamental.go")
  mainFile, err := os.Create(mainPath)
  if err != nil {
    log.Deadly(err)
  }
  defer mainFile.Shut()

  routesFilePath := filepath.Be part of(routesPath, "routes.go")
  routesFile, err := os.Create(routesFilePath)
  if err != nil {
    log.Deadly(err)
  }
  defer routesFile.Shut()
Enter fullscreen mode

Exit fullscreen mode

Acima criamos os arquivos fundamental.go e routes.go.

  • mainPath: determinamos o caminho, usando o mainPath usado para criar a pasta cmd.
  • os.Create(mainPath): Criamos o arquivo, no diretório especificado. (paperwork/my-project/cmd)
  • routesFilePath: determinamos o caminho, usando o routesPath usado para criar a pasta routes.
  • os.Create(routesFilePath): Criamos o arquivo, no diretório especificado. (paperwork/my-project/inside/handler/routes)
  • defer routesFile.Shut(): Fechamos o arquivo, defer, usando essa palavra reservada do GO, garantimos que a última coisa a acontecer é fechar o arquivo. Veja mais sobre o defer aqui.



Escrevendo nos arquivos

Com as pastas e arquivos criados, agora vamos escrever nos arquivos fundamental.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 deal fundamental

  import "fmt"

  func fundamental() {
    fmt.Println("Hey World!")
  }
  `)

    mainFile, err := os.OpenFile(mainPath, os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
      return err
    }
    defer mainFile.Shut()

    _, err = mainFile.Write(packageContent)
    if err != nil {
      return err
    }

    return nil
  }
Enter fullscreen mode

Exit fullscreen mode

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 Hey World.

  • packageContent: Criamos o código que vai ser escrito no arquivo.
  • os.OpenFile: Abrimos o arquivo especificado em mainPath.
  • defer mainFile.Shut(): Fechamos o arquivo por último com defer.
  • 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 deal routes

  // Seu código aqui
  `)

    routesFile, err := os.OpenFile(routesFilePath, os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
      return err
    }
    defer routesFile.Shut()

    _, err = routesFile.Write(packageContent)
    if err != nil {
      return err
    }

    return nil
  }
Enter fullscreen mode

Exit fullscreen mode

Fazemos o mesmo para o arquivo routes.go.

Agora basta chamar as novas funções na função fundamental, ficando assim:

  mainPath := filepath.Be part of(cmdPath, "fundamental.go")
  mainFile, err := os.Create(mainPath)
  if err != nil {
    log.Deadly(err)
  }
  defer mainFile.Shut()
  if err := WriteMainFile(mainPath); err != nil {
    log.Deadly(err)
  }

  routesFilePath := filepath.Be part of(routesPath, "routes.go")
  routesFile, err := os.Create(routesFilePath)
    if err != nil {
    log.Deadly(err)
  }
  defer routesFile.Shut()
  if err := WriteRoutesFile(routesFilePath); err != nil {
    log.Deadly(err)
  }
Enter fullscreen mode

Exit fullscreen mode



Código closing

  package deal fundamental

  import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "path/filepath"

    "github.com/spf13/cobra"
  )

  func fundamental() {
    var rootCommand = &cobra.Command{}
    var projectName, projectPath string

    var cmd = &cobra.Command{
      Use:   "create",
      Brief: "Create boilerplate for a brand new challenge",
      Run: func(cmd *cobra.Command, args []string) {
        if projectName == "" {
          fmt.Println("You need to provide a challenge identify.")
          return
        }
        if projectPath == "" {
          fmt.Println("You need to provide a challenge path.")
          return
        }
        fmt.Println("Creating challenge...")

        globalPath := filepath.Be part of(projectPath, projectName)

        if _, err := os.Stat(globalPath); err == nil {
          fmt.Println("Undertaking listing already exists.")
          return
        }
        if err := os.Mkdir(globalPath, os.ModePerm); err != nil {
          log.Deadly(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.Deadly(err)
        }

        cmdPath := filepath.Be part of(globalPath, "cmd")
        if err := os.Mkdir(cmdPath, os.ModePerm); err != nil {
          log.Deadly(err)
        }
        internalPath := filepath.Be part of(globalPath, "inside")
        if err := os.Mkdir(internalPath, os.ModePerm); err != nil {
          log.Deadly(err)
        }
        handlerPath := filepath.Be part of(internalPath, "handler")
        if err := os.Mkdir(handlerPath, os.ModePerm); err != nil {
          log.Deadly(err)
        }
        routesPath := filepath.Be part of(handlerPath, "routes")
        fmt.Println(routesPath)
        if err := os.Mkdir(routesPath, os.ModePerm); err != nil {
          log.Deadly(err)
        }

        mainPath := filepath.Be part of(cmdPath, "fundamental.go")
        mainFile, err := os.Create(mainPath)
        if err != nil {
          log.Deadly(err)
        }
        defer mainFile.Shut()
        if err := WriteMainFile(mainPath); err != nil {
          log.Deadly(err)
        }

        routesFilePath := filepath.Be part of(routesPath, "routes.go")
        routesFile, err := os.Create(routesFilePath)
        if err != nil {
          log.Deadly(err)
        }
        defer routesFile.Shut()
        if err := WriteRoutesFile(routesFilePath); err != nil {
          log.Deadly(err)
        }
      },
    }

    cmd.Flags().StringVarP(&projectName, "identify", "n", "", "Title of the challenge")
    cmd.Flags().StringVarP(&projectPath, "path", "p", "", "Path the place the challenge will probably be created")

    rootCommand.AddCommand(cmd)
    rootCommand.Execute()
  }

  func WriteMainFile(mainPath string) error {
    packageContent := []byte(`package deal fundamental

  import "fmt"

  func fundamental() {
    fmt.Println("Hey World!")
  }
  `)

    mainFile, err := os.OpenFile(mainPath, os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
      return err
    }
    defer mainFile.Shut()

    _, err = mainFile.Write(packageContent)
    if err != nil {
      return err
    }

    return nil
  }

  func WriteRoutesFile(routesFilePath string) error {
    packageContent := []byte(`package deal routes

  // Seu código aqui
  `)

    routesFile, err := os.OpenFile(routesFilePath, os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
      return err
    }
    defer routesFile.Shut()

    _, err = routesFile.Write(packageContent)
    if err != nil {
      return err
    }

    return nil
  }
Enter fullscreen mode

Exit fullscreen mode



Testando a CLI

Bom, com tudo pronto, vamos testar! Para isso vamos compilar nosso código com o bom e velho go construct.

  go construct -o cli .
Enter fullscreen mode

Exit fullscreen mode

Executando a CLI:

  ./cli create -n my-project -p ~/paperwork
Enter fullscreen mode

Exit fullscreen mode

Vamos ter o retorno:

  Creating challenge...
  go: creating new go.mod: module my-project
Enter fullscreen mode

Exit fullscreen mode

Acessando nosso projeto e abrindo no Visible Studio Code com:

  cd /paperwork/my-project && code .
Enter fullscreen mode

Exit fullscreen mode

Teremos noss boilerplate criado:

Final Project

Se rodarmos o projeto criado through CLI, podemos ver que tudo funciona.

  go run cmd/fundamental.go

  output:
    Hey World!
Enter fullscreen mode

Exit fullscreen mode

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!



Hyperlink do repositório

repositório do projeto
link do projeto no meu weblog

Add a Comment

Your email address will not be published. Required fields are marked *

Want to Contribute to us or want to have 15k+ Audience read your Article ? Or Just want to make a strong Backlink?