DEV Community

Cover image for O poder do CLI com Golang e Cobra CLI
Wiliam V. Joaquim
Wiliam V. Joaquim

Posted on • Edited on

10 3 3 3 4

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 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"))
    }
  }
Enter fullscreen mode Exit fullscreen mode

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

Se rodarmos:

  go run main.go -h
Enter fullscreen mode Exit fullscreen mode

Recebemos:

  -date
    Exibir a data 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 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
Enter fullscreen mode Exit fullscreen mode

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:

Project structure

Agora vamos baixar o pacote Cobra com o comando:

  go get -u github.com/spf13/cobra@latest
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 main.go que 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()
  }
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 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 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 main.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 .
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:

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Vamos rodar agora passando nossas flags:

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

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
  }
Enter fullscreen mode Exit fullscreen mode

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)
  }
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 ~/documents
Enter fullscreen mode Exit fullscreen mode

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)
  }
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 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 ser go 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 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, 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)
  }
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 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()
Enter fullscreen mode Exit fullscreen mode

Acima criamos os arquivos main.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. (documents/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. (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 o defer aqui.

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
  }
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 Hello World.

  • packageContent: Criamos o código que vai ser escrito no arquivo.
  • os.OpenFile: Abrimos o arquivo especificado em mainPath.
  • defer mainFile.Close(): 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 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
  }
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

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
  }
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 build.

  go build -o cli .
Enter fullscreen mode Exit fullscreen mode

Executando a CLI:

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

Vamos ter o retorno:

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

Acessando nosso projeto e abrindo no Visual Studio Code com:

  cd /documents/my-project && code .
Enter fullscreen mode Exit fullscreen mode

Teremos noss boilerplate criado:

Final Project

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

  go run cmd/main.go

  output:
    Hello 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!

Link do repositório

repositório do projeto
link do projeto no meu blog

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more