Cómo crear aplicaciones de línea de comandos con Node.js

Como desarrollador, es probable que pases la mayor parte de tu tiempo en tu terminal, escribiendo comandos que te ayuden a realizar algunas tareas.
Algunos de estos comandos vienen integrados en su sistema operativo, mientras que otros se instalan a través de algún asistente de terceros como npm o brew, o incluso descargando un binario y agregándolo a su $PATH
.
Un buen ejemplo de aplicaciones de uso común incluye npm, eslint, typescript y generadores de proyectos, como Angular CLI, Vue CLI o Create React App.
En este tutorial crearás dos pequeñas aplicaciones CLI en Node.js:
- Herramienta de cita del día que recupera citas del día de https://quotes.rest/qod.
- Una aplicación de lista de tareas pendientes que utiliza JSON para guardar datos.
Prerrequisitos
Para completar este tutorial, necesitarás:
- Un entorno de desarrollo local para Node.js. Siga Cómo instalar Node.js y crear un entorno de desarrollo local
Paso 1 – Entender el asunto
Cada vez que mires cualquier archivo de script, verás caracteres como estos al comienzo del archivo:
file.sh#!/usr/bin/env sh
O esto:
file.py#!/usr/bin/env python -c
Sirven como una forma para que el cargador de programas de su sistema operativo localice y utilice el intérprete correcto para analizar su archivo ejecutable. Sin embargo, esto solo funciona en sistemas Unix.
De Wikipedia:
En informática, un shebang es la secuencia de caracteres que consta de los caracteres signo de número y signo de exclamación (#!) al comienzo de un script.
NodeJS tiene sus propios caracteres shebang compatibles.
Crea un nuevo archivo en tu editor llamado logger.js
:
- nano logger.js
Añade el siguiente código:
#!/usr/bin/env nodeconsole.log("I am a logger")
La primera línea le indica al cargador del programa que analice este archivo con NodeJS. La segunda línea imprime texto en la pantalla.
Puedes intentar ejecutar el archivo escribiendo esto en tu terminal. Recibirás un mensaje que indica que se ha denegado el permiso para la ejecución.
- ./logger
Outputzsh: permission denied: ./logger
Debes darle permisos de ejecución al archivo. Puedes hacerlo con
- chmod +x logger
- ./logger
Esta vez verás el resultado.
OutputI am a logger
Podrías haber ejecutado este programa con node logger
, pero agregar el shebang y hacer que el programa sea ejecutable con su propio comando te permite evitar escribir node
para ejecutarlo.
Creando la aplicación Frase del día
Vamos a crear un directorio y llamarlo qod
. Y dentro, crear una instancia de una aplicación NodeJs.
- mkdir qod
- cd qod
- npm init -y
A continuación, sabemos que necesitamos realizar solicitudes al servidor de cotizaciones, por lo que podríamos usar bibliotecas existentes para hacer precisamente esto. Usaremos axios
npm install --save axios
También agregaremos una tiza, una biblioteca para ayudarnos a imprimir color en la terminal.
npm install --save chalk
Luego escribimos la lógica necesaria para recuperar estas citas.
Crea un nuevo archivo llamado qod
:
- nano qod
Agregue el siguiente código al qod
archivo para especificar el shebang, cargar las bibliotecas y almacenar la URL de la API:
qod
#!/usr/bin/env nodeconst axios = require('axios');const chalk = require('chalk');const url = "https://quotes.rest/qod";
A continuación, agregue este código para realizar una GET
solicitud a la API:
[label qod]// make a get request to the urlaxios({ method: 'get', url: url, headers: { 'Accept': 'application/json' }, // this api needs this header set for the request}).then(res = { const quote = res.data.contents.quotes[0].quote const author = res.data.contents.quotes[0].author const log = chalk.green(`${quote} - ${author}`) // we use chalk to set the color green on successful response console.log(log)}).catch(err = { const log = chalk.red(err) // we set the color red here for errors. console.log(log)})
Guarde el archivo.
Cambie los permisos del archivo para que sea ejecutable:
- chmod +x qod
Luego ejecuta la aplicación:
./qod
Verás una cita:
OutputThe best way to not feel hopeless is to get up and do something. Don’t wait for good things to happen to you. If you go out and make some good things happen, you will fill the world with hope, you will fill yourself with hope. - Barack Obama
Este ejemplo muestra que puede utilizar bibliotecas externas en sus aplicaciones CLI.
Ahora vamos a crear un programa CLI que guarde datos.
Creando una lista de tareas pendientes
Esto será un poco más complejo, ya que implicará el almacenamiento y la recuperación de datos. Esto es lo que estamos tratando de lograr.
- Necesitamos tener un comando llamado
todo
- El comando aceptará cuatro argumentos.
new
,get
,complete
, yhelp
.
Así que los comandos disponibles serán
./todo new // create a new todo./todo get // get a list of all your todos./todo complete // complete a todo item../todo help // print the help text
Cree un directorio llamado todo
y cree una instancia de una aplicación Node.js:
- mkdir todo
- cd todo
- npm install -y
A continuación, vuelve a instalar la tiza para que puedas iniciar sesión con colores.
npm install --save chalk
Lo primero que vamos a hacer es asegurarnos de que tenemos estos comandos disponibles. Para que funcionen, usaremos el proceso/argv de NodeJs, que devuelve una matriz de cadenas con los argumentos de la línea de comandos. La process.argv
propiedad devuelve una matriz que contiene los argumentos de la línea de comandos que se pasaron cuando se inició el proceso de Node.js.
Crea el archivo todo
:
- nano todo
Añade esto al archivo todo.
hacer
#!/usr/bin/env nodeconsole.log(process.argv)
Otorgue permisos de ejecución al archivo y luego ejecútelo con un nuevo comando.
- chmod +x ./todo
- ./todo new
Obtendrás este resultado:
Output[ '/Users/sammy/.nvm/versions/node/v8.11.2/bin/node', '/Users/sammy/Dev/scotch/todo/todo', 'new' ]
Observe que las dos primeras cadenas de la matriz son el intérprete y la ruta completa del archivo al programa. El resto de la matriz contiene los argumentos pasados; en este caso, es new
.
Para estar seguros, vamos a restringirlos, de modo que solo podamos aceptar el número correcto de argumentos, que es uno, y solo pueden ser new
, get
y complete
.
Modifique el todo
archivo para que se parezca al siguiente:
hacer
#!/usr/bin/env nodeconst chalk = require('chalk')const args = process.argv// usage represents the help guideconst usage = function() { const usageText = ` todo helps you manage you todo tasks. usage: todo command commands can be: new: used to create a new todo get: used to retrieve your todos complete: used to mark a todo as complete help: used to print the usage guide ` console.log(usageText)}// used to log errors to the console in red colorfunction errorLog(error) { const eLog = chalk.red(error) console.log(eLog)}// we make sure the length of the arguments is exactly threeif (args.length 3) { errorLog(`only one argument can be accepted`) usage()}
Primero asignamos los argumentos de la línea de comando a una variable y luego verificamos en la parte inferior que la longitud no sea mayor a tres.
También hemos añadido una usage
cadena que imprimirá lo que espera la aplicación de línea de comandos. Ejecute la aplicación con parámetros incorrectos como se muestra a continuación.
- ./todo new app
Outputonly one argument can be acceptedtodo helps you manage you todo tasks.usage: todo command commands can be: new: used to create a new todo get: used to retrieve your todos complete: used to mark a todo as complete help: used to print the usage guide
Si lo ejecuta con un parámetro, no imprimirá nada, lo que significa que el código pasa.
A continuación, debemos asegurarnos de que solo se esperan los cuatro comandos y que todo lo demás se imprimirá como no válido.
Agregue una lista de los comandos en la parte superior del archivo:
hacer
const commands = ['new', 'get', 'complete', 'help']
Y luego verifique con el comando pasado después de haber verificado la longitud:
hacer
...if (commands.indexOf(args[2]) == -1) { errorLog('invalid command passed') usage()}
Ahora, si ejecutamos la aplicación con un comando no válido, obtenemos esto.
- ./todo ne
Outputinvalid command passed todo helps you manage you todo tasks. usage: todo command commands can be: new: used to create a new todo get: used to retrieve your todos complete: used to mark a todo as complete help: used to print the usage guide
Ahora implementemos el help
comando llamando a la usage
función. Agreguemos esto al archivo de tareas pendientes:
hacer
//...switch(args[2]) { case 'help': usage() break case 'new': break case 'get': break case 'complete': break default: errorLog('invalid command passed') usage()}//...
Tenemos una switch
declaración que llamará a funciones según el comando que se haya llamado. Si observas con atención, notarás que el help
caso solo llama a la función de uso.
El new
comando creará un nuevo elemento de la lista de tareas pendientes y lo guardará en un archivo json. La biblioteca que usaremos es lowdb. Podríamos escribir fácilmente funciones para leer y escribir en un archivo json, si quisiéramos.
Instalar lowdb
- npm install --save lowdb
Agreguemos [readline](https://nodejs.org/api/readline.html)
dependencias lowdb
para que nos ayuden a almacenar datos. El código de LowDB es estándar en su página de GitHub.
hacer
//...const rl = require('readline');const low = require('lowdb')const FileSync = require('lowdb/adapters/FileSync')const adapter = new FileSync('db.json')const db = low(adapter)// Set some defaults (required if your JSON file is empty)db.defaults({ todos: []}).write()//...
A continuación, agregaremos una función para solicitar al usuario que ingrese datos.
hacer
//...function prompt(question) { const r = rl.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); return new Promise((resolve, error) = { r.question(question, answer = { r.close() resolve(answer) }); })}//...
Aquí usamos la biblioteca readline para crear una interfaz que nos ayudará a solicitarle al usuario que escriba y luego leer la salida.
A continuación, debemos agregar una función que se llamará cuando un usuario escriba el new
comando:
hacer
//...function newTodo() { const q = chalk.blue('Type in your todon') prompt(q).then(todo = { console.log(todo) })}//...
Usaremos tiza para obtener el color azul de la indicación y luego registraremos el resultado.
Por último, llama a la función en el new
caso.
hacer
// ...switch(args[2]) { //... case 'new': newTodo() break// ...}// ...
Cuando ejecutes la aplicación ahora con el nuevo comando, se te solicitará que agregues una tarea pendiente. Escribe y presiona Enter.
- ./todo new
OutputType in your todoThis my todo aaaaaaw yeahThis my todo aaaaaaw yeah
Deberías ver algo similar a esto.
Tenga en cuenta también que db.json
se ha creado un archivo en su sistema de archivos y que tiene una propiedad todos.
A continuación, agreguemos la lógica para agregar una tarea pendiente. Modifique la función newTodo.
hacer
//...function newTodo() { const q = chalk.blue('Type in your todon') prompt(q).then(todo = { // add todo db.get('todos') .push({ title: todo, complete: false }) .write() })}//...
Ejecute el código nuevamente.
- ./todo new
OutputType in your todoTake a Scotch course
Si miras tu db.json
, verás que se agregó la tarea pendiente. Agrega dos más, para que podamos recuperarlas en el próximo comando get. Así db.json
se ve el archivo con más registros:
base de datos.json
{ "todos": [ { "title": "Take a Scotch course", "complete": false }, { "title": "Travel the world", "complete": false }, { "title": "Rewatch Avengers", "complete": false } ]}
Después de crear el new
comando, ya debería tener una idea de cómo implementarlo get
.
Crea una función que recuperará los todos.
hacer
//...function getTodos() { const todos = db.get('todos').value() let index = 1; todos.forEach(todo = { const todoText = `${index++}. ${todo.title}` console.log(todoText) })}//...// switch statementsswitch(args[2]) {//...case 'get':getTodos()break//...}//....
Ejecute el comando nuevamente:
- ./todo get
Al ejecutar la aplicación ahora se obtendrá este resultado:
Output1. Take a Scotch course2. Travel the world3. Rewatch Avengers
Puedes hacer que el color sea verde usando chalk.green
.
A continuación, agrega el complete
comando, que es un poco complicado.
Puedes hacerlo de dos maneras.
- Cada vez que un usuario escribe
./todo complete
, podemos enumerar todas las tareas pendientes y luego pedirle que escriba el número o la tecla que desea marcar como completada. - Podemos agregar otro parámetro, de modo que un usuario pueda escribir
./todo get
y luego elegir la tarea que desea marcar como completada con un parámetro, como por ejemplo./todo complete 1
.
Como aprendiste cómo hacer el primer método cuando implementaste el new
comando, veremos la opción 2.
Con esta opción, el comando ./todo complete 1
, no superará nuestra comprobación de validez para la cantidad de comandos proporcionados. Por lo tanto, primero debemos solucionar este problema. Cambie la función que comprueba la longitud de los argumentos por esta:
hacer
//...// we make sure the length of the arguments is exactly threeif (args.length 3 args[2] != 'complete') { errorLog('only one argument can be accepted') usage() return}///...
Este enfoque utiliza tablas de verdad, donde TRUE FALSE
será igual a FALSE
y se omitirá el código cuando complete
se pase.
Luego tomaremos el valor del nuevo argumento y haremos que el valor de todo sea completado:
hacer
//...function completeTodo() { // check that length if (args.length != 4) { errorLog("invalid number of arguments passed for complete command") return } let n = Number(args[3]) // check if the value is a number if (isNaN(n)) { errorLog("please provide a valid number for complete command") return } // check if correct length of values has been passed let todosLength = db.get('todos').value().length if (n todosLength) { errorLog("invalid number passed for complete command.") return } // update the todo item marked as complete db.set(`todos[${n-1}].complete`, true).write()}//...
Además, actualice la switch
declaración para incluir el complete
comando:
hacer
//...case 'complete': completeTodo() break//...
Cuando ejecutes esto con ./todo complete 2
, notarás que tu db.json
ha cambiado a esto, marcando la segunda tarea como completada:
base de datos.json
{ "todos": [ { "title": "Take a Scotch course", "complete": false }, { "title": "Travel the world", "complete": true }, { "title": "Rewatch Avengers", "complete": false } ]}
Lo último que tenemos que hacer es cambiar ./todo get
para que solo se muestren las tareas que ya se han realizado. Para ello, utilizaremos emojis. Modifique getTodos
con este código:
hacer
//...function getTodos() { const todos = db.get('todos').value() let index = 1; todos.forEach(todo = { let todoText = `${index++}. ${todo.title}` if (todo.complete) { todoText += ' ✔ ️' // add a check mark } console.log(chalk.strikethrough(todoText)) }) return}//...
Cuando escribas ahora ./todo get
verás esto.
Output1. Take a Scotch course2. Travel the world ✔ ️3. Rewatch Avengers
Conclusión
Ha escrito dos aplicaciones CLI en Node.js.
Una vez que la aplicación esté funcionando, coloque el archivo en una bin
carpeta. De esta manera, npm sabrá cómo trabajar con el ejecutable cuando lo distribuya. Además, independientemente de dónde coloque el ejecutable, debe actualizar package.json
la propiedad bin.
El objetivo de este artículo fue analizar cómo se crean las aplicaciones CLI con nodejs vanilla, pero cuando se trabaja en el mundo real, sería más productivo utilizar bibliotecas.
Aquí tienes una lista de bibliotecas útiles para ayudarte a escribir increíbles aplicaciones CLI, que puedes publicar en npm.
- vopral: marco de trabajo CLI interactivo con todas las funciones
- Meow – Biblioteca auxiliar de CLI
- commanderjs – biblioteca de línea de comandos
- minimista – para análisis de argumentos
- yargs – análisis de argumentos
Y ni hablar de las bibliotecas como Chalk que nos ayudaron con los colores.
Como ejercicio adicional, intente agregar un Delete
comando a la CLI.
Deja una respuesta