Una de mis cosas favoritas de Node es lo f谩cil que es crear herramientas simples de interfaz de l铆nea de comandos (CLI). Entre el an谩lisis de argumentos con yargs para administrar herramientas con npm, Node simplemente lo hace f谩cil.
Algunos ejemplos de los tipos de herramientas a los que me refiero son:
Cuando se instala (con el -g
opci贸n), estos paquetes se pueden ejecutar desde cualquier lugar en la l铆nea de comandos y funcionan de manera muy similar a las herramientas integradas de Unix.
He estado creando algunas aplicaciones Node.js para la l铆nea de comandos 煤ltimamente y pens茅 que podr铆a ser 煤til escribir una publicaci贸n sobre ellas para ayudarlo a comenzar. Entonces, a lo largo de este art铆culo, le mostrar茅 c贸mo crear una herramienta de l铆nea de comandos para obtener datos de ubicaci贸n para direcciones IP y URL.
Si ha visto el art铆culo de Abuso de pila sobre el aprendizaje de Node.js, puede recordar que creamos un paquete llamado twenty
que ten铆a una funcionalidad similar. Partiremos de ese proyecto y lo convertiremos en una herramienta CLI adecuada con m谩s funcionalidad.
Configurar el proyecto
Contenido
Comencemos creando un nuevo directorio y configurando el proyecto usando npm:
$ mkdir twenty
$ npm init
Presione enter para todas las indicaciones en el 煤ltimo comando, y deber铆a tener su package.json
archivo.
Tenga en cuenta que dado que ya he tomado el nombre del paquete twenty
en npm, tendr谩 que cambiarle el nombre por otro si realmente desea publicar. O tambi茅n podr铆as alcance tu proyecto.
Luego, crea el index.js
archivo:
$ touch index.js
Esto es todo lo que realmente necesitamos para comenzar por ahora, y agregaremos al proyecto a medida que avancemos.
Analizar argumentos
La mayor铆a de las aplicaciones CLI aceptan argumentos del usuario, que es la forma m谩s com煤n de obtener informaci贸n. En la mayor铆a de los casos, analizar los argumentos no es demasiado dif铆cil, ya que generalmente solo hay un pu帽ado de comandos y banderas. Pero a medida que la herramienta se vuelve m谩s compleja, se agregar谩n m谩s indicadores y comandos, y el an谩lisis de argumentos puede volverse sorprendentemente dif铆cil.
Para ayudarnos con esto, usaremos un paquete llamado yargs
, que es el sucesor del popular optimista paquete.
yargs
fue creado para ayudarlo a analizar los comandos del usuario, como este:
var argv = require('yargs').argv;
Ahora cadenas de opciones complejas como node index.js install -v --a=22 -cde -x derp
se puede acceder f谩cilmente:
var argv = require('yargs').argv;
argv._[0] // 'install'
argv.v // true
argv.a // 22
argv.c // true
argv.d // true
argv.e // true
argv.x // 'derp'
yargs
incluso lo ayudar谩 a especificar la interfaz de comando, por lo que si la entrada del usuario no cumple con ciertos requisitos, le mostrar谩 un mensaje de error. Entonces, por ejemplo, podemos decir yargs
queremos al menos 2 argumentos:
var argv = require('yargs')
.demand(2)
.argv
Y si el usuario no proporciona al menos dos, ver谩 este mensaje de error predeterminado:
$ node index.js foo
Not enough non-option arguments: got 1, need at least 2
Hay mucho m谩s para yargs
que solo esto, as铆 que consulte el archivo L茅ame para obtener m谩s informaci贸n.
Xa twenty
, tomaremos algunos argumentos opcionales, como una direcci贸n IP y algunas banderas. Por ahora, usaremos yargs
Me gusta esto:
var argv = require('yargs')
.alias('d', 'distance')
.alias('j', 'json')
.alias('i', 'info')
.usage('Usage: $0 [options]')
.example('$0 -d 8.8.8.8', 'find the distance (km) between you and Google DNS')
.describe('d', 'Get distance between IP addresses')
.describe('j', 'Print location data as JSON')
.describe('i', 'Print location data in human readable form')
.help('h')
.alias('h', 'help')
.argv;
Dado que ninguno de nuestros argumentos es obligatorio, no utilizaremos .demand()
, pero usamos .alias()
, que dice yargs
que el usuario puede utilizar la forma corta o larga de cada bandera. Tambi茅n hemos agregado documentaci贸n de ayuda para mostrarle al usuario cu谩ndo la necesita.
Estructurando la aplicaci贸n
Ahora que podemos obtener la entrada del usuario, 驴c贸mo tomamos esa entrada y la traducimos a un comando con los argumentos opcionales? Existen algunos m贸dulos dise帽ados para ayudarlo a hacer esto, que incluyen:
Con muchos de estos marcos, el an谩lisis de argumentos se realiza realmente por usted, por lo que ni siquiera necesita usar yargs
. Y en commander
es el caso, la mayor parte de su funcionalidad se parece mucho yargs
, aunque proporciona formas de enrutar comandos a funciones.
Dado que nuestra aplicaci贸n es bastante simple, nos limitaremos a usar yargs
Por ahora.
A帽adiendo el c贸digo
No pasaremos demasiado tiempo aqu铆, ya que es espec铆fico solo para nuestra aplicaci贸n CLI, pero aqu铆 est谩 el c贸digo espec铆fico para nuestra aplicaci贸n:
var dns = require('dns');
var request = require('request');
var ipRegex = /(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/;
var toRad = function(num) {
return num * (Math.PI / 180);
};
var getIpInfo = function(server, callback) {
var ipinfo = function(p, cb) {
request('http://ipinfo.io/' + p, function(err, response, body) {
var json = JSON.parse(body);
cb(err, json);
});
};
if (!server) {
return ipinfo('json', callback);
} else if (!server.match(ipRegex)) {
return dns.lookup(server, function(err, data) {
ipinfo(data, callback);
});
} else {
return ipinfo(server, callback);
}
};
var ipDistance = function(lat1, lon1, lat2, lon2) {
// Earth radius in km
var r = 6371;
var dLat = toRad(lat2 - lat1);
var dLon = toRad(lon2 - lon1);
lat1 = toRad(lat1);
lat2 = toRad(lat2);
var a = Math.sin(dLat / 2.0) * Math.sin(dLat / 2.0) +
Math.sin(dLon / 2.0) * Math.sin(dLon / 2.0) * Math.cos(lat1) * Math.cos(lat2);
var c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1.0 - a));
return r * c;
};
var findLocation = function(server, callback) {
getIpInfo(server, function(err, data) {
callback(null, data.city + ', ' + data.region);
});
};
var findDistance = function(ip1, ip2, callback) {
var lat1, lon1, lat2, lon2;
getIpInfo(ip1, function(err, data1) {
var coords1 = data1.loc.split(',');
lat1 = Number(coords1[0]);
lon1 = Number(coords1[1]);
getIpInfo(ip2, function(err, data2) {
var coords2 = data2.loc.split(',');
lat2 = Number(coords2[0]);
lon2 = Number(coords2[1]);
var dist = ipDistance(lat1, lon1, lat2, lon2);
callback(null, dist);
});
});
};
Para el c贸digo fuente completo, puede encontrar el repositorio aqu铆.
Lo 煤nico que nos queda por hacer con el c贸digo es conectar los argumentos de la CLI con el c贸digo de la aplicaci贸n anterior. Para que sea m谩s f谩cil, pondremos todo esto en una funci贸n llamada cli()
, que usaremos m谩s adelante.
Encapsulando el an谩lisis de argumentos y la asignaci贸n de comandos dentro cli()
ayuda a mantener el c贸digo de la aplicaci贸n por separado, lo que permite que este c贸digo se importe como una biblioteca con require()
.
var cli = function() {
var argv = require('yargs')
.alias('d', 'distance')
.alias('j', 'json')
.alias('i', 'info')
.usage('Usage: $0 [IP | URL] [--d=IP | URL] [-ij]')
.example('$0 -d 8.8.8.8', 'find the distance (km) between you and Google DNS')
.describe('d', 'Get distance between IP addresses')
.describe('j', 'Print location data as JSON')
.describe('i', 'Print location data in human readable form')
.help('h')
.alias('h', 'help')
.argv;
var path="json";
if (argv._[0]) {
path = argv._[0];
}
if (argv.d) {
findDistance(path, argv.d, function(err, distance) {
console.log(distance);
});
} else if (argv.j) {
getIpInfo(path, function(err, data) {
console.log(JSON.stringify(data, null, 4));
});
} else if (argv.i) {
getIpInfo(path, function(err, data) {
console.log('IP:', data.ip);
console.log('Hostname:', data.hostname);
console.log('City:', data.city);
console.log('Region:', data.region);
console.log('Postal:', data.postal);
console.log('Country:', data.country);
console.log('Coordinates:', data.loc);
console.log('ISP:', data.org);
});
} else {
findLocation(path, function(err, location) {
console.log(location);
});
}
};
exports.info = getIpInfo;
exports.location = findLocation;
exports.distance = findDistance;
exports.cli = cli;
Aqu铆 puede ver que b谩sicamente solo usamos if...else
declaraciones para determinar qu茅 comando ejecutar. Podr铆a ser mucho m谩s elegante y usar Flatiron para asignar cadenas de expresiones regulares a comandos, pero eso es un poco exagerado para lo que estamos haciendo aqu铆.
Haci茅ndolo ejecutable
Para que podamos ejecutar la aplicaci贸n, necesitamos especificar algunas cosas en nuestro package.json
archivo, como donde reside el ejecutable. Pero primero, creemos el ejecutable y su c贸digo. Crea un archivo llamado twenty
en el directorio twenty/bin/
y agr茅guele esto:
#!/usr/bin/env node
require('../index').cli();
El shebang#!/usr/bin/env node
) le dice a Unix c贸mo ejecutar el archivo, permiti茅ndonos omitir el node
prefijo. La segunda l铆nea simplemente carga el c贸digo desde arriba y llama al cli()
funci贸n.
En package.json
, agregue el siguiente JSON:
"bin": {
"twenty": "./bin/twenty"
}
Esto solo le dice a npm d贸nde encontrar el ejecutable al instalar el paquete con el -g
bandera (global).
As铆 que ahora, si instala twenty
como global …
$ npm install -g twenty
… luego puede obtener las ubicaciones de los servidores y las direcciones IP:
$ twenty 198.41.209.141 #reddit
San Francisco, California
$ twenty rackspace.com
San Antonio, Texas
$ twenty usa.gov --j
{
"ip": "216.128.241.47",
"hostname": "No Hostname",
"city": "Phoenix",
"region": "Arizona",
"country": "US",
"loc": "33.3413,-112.0598",
"org": "AS40289 CGI TECHNOLOGIES AND SOLUTIONS INC.",
"postal": "85044"
}
$ twenty Pharos.sh.com
Ashburn, Virginia
Y ah铆 lo tiene, el servidor de abuso de pila se encuentra en Asburn, Virginia. Interesante =)
Para obtener el c贸digo fuente completo, consulte el proyecto en Github.