Criando sua CLI com Node.js

Henrique Kuwai
henriquekuwai
Published in
10 min readFeb 11, 2019

Olá pessoal!

Qualquer desenvolvedor certamente precisa ou precisará ter contato com CLIs (Command Line Interface) durante o seu dia-a-dia. Ferramentas como Git, NPM, Maven, Heroku, Surge, entre outras, todas trazem CLIs para facilitar as tarefas do desenvolvedor; seja para manipular arquivos, como para trazer/enviar informações, conectar com APIs, entre outros.

Por meio de shell scripts, é fácil realizar tarefas envolvendo arquivos do sistema, no entanto, programar em shell script nem sempre é uma tarefa tão simples assim. Para mim, que estou plenamente acostumado com JavaScript e já conhecendo as capacidade do Node.js, não demorou para vir a pergunta (para mim mesmo):

Será que é possível criar CLIs com JavaScript?

Com poucos minutos de pesquisa, a resposta foi um grande sim! Na verdade, o que não dá pra criar com JavaScript hoje em dia, não é mesmo?

Não só é possível criar CLIs com o Node.js como existe uma quantidade enorme de frameworks e bibliotecas para facilitar as tarefas mais comuns quando se fala de interfaces de linha de comando, como input de dados, manipulação dos comandos e flags, exibição das informações de uma maneira mais visual, execução de comandos mais “baixo-nível”, etc.

Para o exemplo que vou trazer aqui, vamos utilizar as seguintes bibliotecas:

Vamos fazer um clássico to-do list, mas como CLI.

Criando a base do CLI

Primeiro crie a pasta do seu projeto (ex: /todo-cli) e vamos iniciar o projeto utilizando NodeJS 8+, já instalando os packages que vamos utilizar com o npm ou o yarn (no caso, utilizarei o yarn):

> yarn init
> yarn add chalk@2.4.2 commander@2.19.0 figlet@1.2.1 inquirer@6.2.2 shelljs@0.8.3 cli-table@0.3.1

Agora, com tudo instalado, vamos criar um index.js com o seguinte conteúdo:

  • Na primeiríssima linha, adicionamos um comentário iniciado com hashtag para que, em sistemas *nix, este script seja interpretado utilizando node. Em sistemas Windows isso será simplesmente ignorado.
  • Depois, nas linhas 3 e 4, damos o require no commander e depois no package.json, apenas com o intuito de pegar a versão do projeto.
  • Na linha 6, setamos a versão do CLI, utilizando a função version do Commander.js.
  • A partir da linha 8, colocamos nosso primeiro comando do CLI: o comando de add. Primeiro você utiliza o métodocommand, passando uma string, e os parâmetros esperados no comando devem ser encapsulados com <> (se forem obrigatórios) e com [] se forem opcionais.
  • Depois, com o método description, setei uma descrição para o comando (isso será útil para a criação automática — pelo commander — da flag--help). Perceba que utilizei a marcação [todo], e depois esse parâmetro é passado na função de callback do método action .
  • E na última linha, o commander requere que passemos o process.argv, algo que o Node.js já disponibiliza para nós, para ele poder interpretar os comandos.

Já podemos fazer nosso primeiro teste, executando node index.js add teste, e o output será mais ou menos assim:

Mas, peraí. A ideia era criar uma CLI, onde eu pudesse executar em qualquer lugar da minha máquina! Dessa forma tenho que passar o caminho do script!

Calma, jovem. Isso você vai aprender exatamente agora:

Registrando seu script como um comando global

Com o script criado, vamos alterar e adicionar um nó novo no package.json:

"bin": {
"todo": "index.js"
}
  • O nó define qual será o comando principal do CLI;
  • O arquivo define qual script será executado ao rodar o comando.

Salve o arquivo e, dentro do seu projeto, execute o comando npm link. Aguarde um momento e… simples assim. Em alguns sistemas, pode ser necessário o uso de sudo, pois ele registra isso de forma global.

Agora você pode utilizar o comando em qualquer local da sua máquina!

E o mais legal disso é que ele mantém um link direto ao seu arquivo, sendo assim, você pode ir desenvolvendo e ir executando o comando para testar, sem precisar de mais nada. :)

Adicionando, de fato, o to-do

Para minimizar o boilerplate aqui do tutorial, optei por realizar de uma forma que não fosse necessário a integração com uma API. Sendo assim, vamos simplesmente gravar e ler os nossos to-dos em um arquivo todos.json.

Vamos fazê-lo com duas possibilidades: caso você passe o argumento [todo], ele já adicionará sem nenhuma pergunta:

  • Nas linhas iniciais, agora eu incluo dois módulos do Node.js: path (mais especificamente a função join) e fs, para manipular arquivos do file system;
  • Na linha 8, guardei numa variável o path do nosso “banco de dados”: todos.json;
  • Nas linhas 10 e 18, criei funções utilitárias para pegar os dados do arquivo e salvar, prevenindo erros caso ele esteja vazio no getJson e parseando como string com separadores no saveJson;
  • A partir da linha 26, simplesmente: pego os dados do todos.json com a função e guardo numa variável, acrescento o to-do passado como objeto, junto ao atributo done, e salvo o arquivo novamente.

Mas como o atributo [todo] está opcional, precisaremos adicionar um tratamento, e aí vem a segunda possibilidade:

  • Na linha 6, incluí o módulo Inquirer, para fazer nossas perguntas ao usuário;
  • Na linha 26, perceba que adicionei a keyword async à função, pois vamos utilizar await dentro dela;
  • Na linha 27, declaro a variável answers;
  • A partir da linha 28, caso o usuário não passe o argumento todo, vamos perguntar ao usuário qual o texto do to-do. O inquirer possui o método prompt, que espera um array de objetos contendo cada “campo” do seu formulário no CLI, tendo como parâmetro obrigatório o type do input — veja todos os tipos [aqui](https://github.com/SBoudrias/Inquirer.js/#examples) e o name, pois depois será utilizado para pegar os dados. Isto retorna uma promise, então podemos utilizar o await por aqui!
  • Finalmente, na linha 40, verificamos: o argumento todo foi passado? Então utilizamos ele; senão, utilize a resposta do Inquirer!

Tratando os erros

O Inquirer permite que você valide os dados inputados, e não deixe o usuário prosseguir enquanto não estiver correto.

Sendo assim, vamos adicionar uma validação para que a pergunta do to-do não passe vazia:

  • Na linha 34, adicionei o atributo validate, que recebe uma função passando o parâmetro value. No caso da minha validação, só quero verificar se o usuário passou qualquer truthy value; caso sim, retorna true; senão, exibe a mensagem de erro para o usuário e não deixa ele prosseguir.

O Inquirer possui ainda um atributo filter, que manipula os dados DEPOIS de inputados. Mas, como qualquer biblioteca na vida, dê uma lidinha na [documentação oficial](https://github.com/SBoudrias/Inquirer.js).

Nosso programa já vai rodar dessa maneira:

A primeira vez, passando o argumento. Na segunda, usando o Inquirer

E nossos dados ficarão assim:

[
{
"title": "teste",
"done": false
},
{
"title": "Comprar pão",
"done": false
}
]

Adicionando mensagens bonitas de retorno

Utilizando o chalk, podemos adicionar cor aos nossos console.log de maneira muito, muito fácil:

  • Na linha 7, agora incluo o módulo Chalk;
  • E na linha 46, retorno a mensagem de sucesso utilizando o método green do chalk. Você pode escolher uma cor, um background e um decoration (como por exemplo, underline ou bold) — veja na [documentação do chalk](https://github.com/chalk/chalk).

E….. tcharam!

Você pode adicionar quantas cores quiser; com template strings isso fica muito mais fácil e legível. :)

E se eu quiser adicionar o to-do com opções adicionais?

Pra isso, podemos utilizar flags! Os famosos argumentos passados com -- ou - depois do comando. O commander nos traz essa possibilidade de maneira EXTREMAMENTE fácil.

Vamos colocar uma opção --status no nosso comando de add. Dessa forma, se a flag for passada no comando, somada a algum parâmetro, o to-do já será salvo com o atributo done: {status-passado-na-flag}.

  • Na linha 27, usamos o método option pra criar a opção --status, junto com a descrição da flag (novamente, será útil para o comando --help). Você pode definir um shorthand (-s) junto com a flag completa;
  • Na linha 28, o último parâmetro sempre será o options, para pegar as flags passadas pelo usuário (podem ser infinitas!);
  • E na linha 44, adiciono um tratamento para pegar o dado inputado e finalizar como booleano, pois os dados aqui sempre virão como String .

Agora, passando a flag --status, o to-do já será salvo com o atributo done: true!

Vamos completar as ações do nosso to-do?

Agora, precisamos de um método para listar nossos to-dos, outro para marcar como feito e outro para marcar como não feito. O básico do funcionamento da interação com a CLI já foi passado, então vou simplesmente exemplificar por aqui:

  • Na linha 22, criei uma função utilitária para pegar os dados e exibir em forma de tabela. Utilizei o módulo cli-table, então apenas pego os dados do JSON, crio a instância definindo os headings e os tamanhos das colunas;
  • A partir da linha 63, criei o comando list, que não precisa receber nenhum parâmetro;
  • Depois passo os dados para a função — perceba que utilizei o chalk para deixar verdinho o status — e exibo para o usuário.

O resultado é este:

Separei a exibição da tabela em uma função, pois vou reaproveitá-la nos métodos de do e undo, que recebem o id do to-do para serem atualizados:

  • Nas linhas 71 e 94, criei os comandos do e undo, respectivamente. Eles são como o comando de add: recebem um parâmetro opcional, manipula o JSON, e exibe a tabela final para o usuário.

O resultado é este:

E já que reaproveitamos o método da tabela, por que não utilizar também no add?

Agora, comandos shell no JavaScript!

Vamos criar um comando totalmente desnecessário — mas para fins didáticos, de backup, apenas com o intuito de aprender a usar o shelljs. Sendo assim, ele executará simplesmente um copy do arquivo todos.json para uma pasta backup utilizando comandos shell.

  • Na linha 9, incluo o módulo shelljs;
  • Na linha 119, comecei a criar o comando backup;
  • Primeiro, tento criar a pasta backup, caso ela não exista, utilizando o comando mkdir do shelljs;
  • Depois, executo o comando de mover o arquivo passando um parâmetro para ele não logar nada no console, apenas se eu quiser, e guardo numa variável;
  • Depois, faço o tratamento de erros: o exec pode retornar 3 atributos: code e stderr (caso dê erro), e stdin, caso dê tudo certo. Exibo as mensagens de acordo.

Lembra do comando --help?

Agora o commander brilha:

Isso é gerado automaticamente por ele, conforme você utiliza os métodos corretos. Lindo, não?

E uma firula com o FigletJS para nosso terminal ficar mais bonito:

  • Na linha 10, inclui o FigletJS;
  • Na linha 39, logo o figlet passando um texto qualquer, utilizando o chalk para deixá-lo com a cor cyan.

E agora sempre que executarmos comandos do nosso CLI, um texto em letras garrafais irá aparecer, dessa forma:

Finalizando…

Passei aqui o básico de como começar seu CLI e definir seus comandos, algumas das bibliotecas mais utilizadas tanto para criar a UI quanto facilitar a criação de inputs de dados, bem como executar ações de shell com JavaScript, mas a partir disso, você pode utilizar CLIs para:

  • Criar componentes no seu app — utilizando um template engine server-side, como o Pug ou Handlebars;
  • Fazer scaffolding de uma estrutura definida de projeto;
  • Enviar arquivos para um servidor (podendo-se utilizar libs de SSH ou SFTP);
  • Executar tarefas monótonas e repetitivas, como talvez realizar um git pull em todos os seus projetos (sim, qualquer comando shell pode ser utilizado com o shelljs);
  • E mais o que sua imaginação permitir :)

O mundo de criação de CLIs com JavaScript é ainda mais vasto do que isso; é possível utilizar até React para potencializá-las (utilizando o [Ink](https://github.com/vadimdemedes/ink)), libs para fazer loaders animados para requisições ou operações assíncronas ([Ora](https://github.com/sindresorhus/ora)), mas provavelmente rende outros artigos.

Agradecimentos especiais aos autores dos artigos abaixo, que me deram um primeiro contato com essa possibilidade:

O conteúdo do tutorial está todo neste repositório:

Qualquer dúvida, não hesite em comentar.

Até a próxima, galera!

--

--