Criando sua CLI com Node.js
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:
- Commander.js, que facilita a criação de comandos e manipulação de flags e options (https://github.com/tj/commander.js/)
- Inquirer.js, um agrupador de inputs no CLI (para inputar dados, checkboxes, etc) (https://github.com/SBoudrias/Inquirer.js/)
- Shelljs, para executar comandos shell via JavaScript (https://github.com/shelljs/shelljs)
- Chalk, para facilitar o log de informações coloridas (https://github.com/chalk/chalk)
- Figletjs, para logar textos em letras garrafais — e pode ser utilizado em conjunto com o chalk! (https://github.com/patorjk/figlet.js)
- CLI Table, para exibir tabelas no terminal (https://github.com/Automattic/cli-table)
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étodoaction
. - 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 nosaveJson
; - 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 atributodone
, 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 utilizarawait
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étodoprompt
, que espera um array de objetos contendo cada “campo” do seu formulário no CLI, tendo como parâmetro obrigatório otype
do input — veja todos os tipos [aqui](https://github.com/SBoudrias/Inquirer.js/#examples) e oname
, pois depois será utilizado para pegar os dados. Isto retorna uma promise, então podemos utilizar oawait
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âmetrovalue
. 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:
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
eundo
, respectivamente. Eles são como o comando deadd
: 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 comandomkdir
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
estderr
(caso dê erro), estdin
, 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:
- https://codeburst.io/building-a-node-js-interactive-cli-3cb80ed76c86
- https://scotch.io/tutorials/build-an-interactive-command-line-application-with-nodejs
O conteúdo do tutorial está todo neste repositório:
Qualquer dúvida, não hesite em comentar.
Até a próxima, galera!