Cómo crear una aplicación CLI de Node.js

Una de mis cosas favoritas absolutas de Node es lo fácil que es crear herramientas simples de interfaz de línea de comandos (CLI). Entre análisis de argumentos con yargs a maná...

Una de mis cosas favoritas absolutas de Node es lo fácil que es crear herramientas simples de interfaz de línea de comandos (CLI). Desde el análisis de argumentos con yargos hasta la gestión de herramientas con npm, Node lo facilita.

Algunos ejemplos de los tipos de herramientas a los que me refiero son:

Cuando se instalan (con la opción -g), estos paquetes se pueden ejecutar desde cualquier lugar de la línea de comandos y funcionan de manera muy similar a las herramientas integradas de Unix.

He estado creando algunas aplicaciones de Node.js para la línea de comandos últimamente y pensé que podría ser útil escribir una publicación 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 Stack Abuse en aprendiendo Node.js, puede recordar que creamos un paquete llamado twenty que tenía una funcionalidad similar . Construiremos a partir de ese proyecto y lo convertiremos en una herramienta CLI adecuada con más funcionalidad.

Configuración del proyecto

Comencemos creando un nuevo directorio y configurando el proyecto usando npm:

1
2
$ mkdir twenty
$ npm init

Presiona enter para todas las indicaciones en el último comando, y deberías tener tu archivo package.json.

  • Tenga en cuenta que dado que ya tomé el nombre del paquete twenty en npm, tendrá que cambiarle el nombre a otro si realmente desea publicar. O también podría alcance su proyecto.*

Luego, crea el archivo index.js:

1
$ touch index.js

Esto es todo lo que realmente necesitamos para comenzar por ahora, y lo agregaremos al proyecto a medida que avancemos.

Análisis de 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 yargos, que es el sucesor del popular [optimista](https://www .npmjs.com/package/optimist) paquete.

yargs fue creado para ayudarte a analizar los comandos del usuario, como este:

1
var argv = require('yargs').argv;

Ahora se puede acceder fácilmente a optstrings complejos como node index.js install -v --a=22 -cde -x derp:

1
2
3
4
5
6
7
8
9
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 decirle a yargs que queremos al menos 2 argumentos:

1
2
3
var argv = require('yargs')
    .demand(2)
    .argv

Y si el usuario no proporciona al menos dos, verá este mensaje de error predeterminado:

1
2
3
$ node index.js foo

Not enough non-option arguments: got 1, need at least 2

Hay mucho más en yargs que solo esto, así que consulta el archivo Léame para obtener más información.

Para twenty, tomaremos algunos argumentos opcionales, como una dirección IP y algunas banderas. Por ahora, usaremos yargs así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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;

Como no se requiere ninguno de nuestros argumentos, no usaremos .demand(), pero sí usamos .alias(), que le dice a yargs que el usuario puede usar la forma corta o larga de cada bandera . También hemos agregado alguna documentación de ayuda para mostrar al usuario cuando la necesite.

Estructurando la aplicación

Ahora que podemos obtener información del usuario, ¿cómo tomamos esa información y la traducimos a un comando con 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 por usted, por lo que ni siquiera necesita usar yargs. Y en el caso de commander's, la mayor parte de su funcionalidad se parece mucho a yargs, aunque proporciona formas de enrutar comandos a funciones.

Dado que nuestra aplicación es bastante simple, por ahora nos quedaremos con yargs.

Agregando el código

No dedicaremos mucho 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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, puedes 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 hacerlo más fácil, pondremos todo esto en una función llamada cli(), que usaremos más adelante.

Encapsular el análisis de argumentos y el mapeo de comandos dentro de cli() ayuda a mantener el código de la aplicación separado, lo que permite importar este código como una biblioteca con require().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 usamos declaraciones if...else para determinar qué comando ejecutar. Podría ser mucho más elegante y usar Flatiron para asignar cadenas de expresiones regulares a los comandos, pero eso es un poco exagerado para lo que estamos haciendo aquí.

Haciéndolo ejecutable

Para que podamos ejecutar la aplicación, debemos especificar algunas cosas en nuestro archivo package.json, como dónde reside el ejecutable. Pero primero, creemos el ejecutable y su código. Cree un archivo llamado twenty en el directorio twenty/bin/ y agréguele esto:

1
2
#!/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 prefijo node. La segunda línea simplemente carga el código de arriba y llama a la función cli().

En package.json, agregue el siguiente JSON:

1
2
3
"bin": {
    "twenty": "./bin/twenty"
}

Esto solo le dice a npm dónde encontrar el ejecutable al instalar el paquete con el indicador -g (global).

Así que ahora, si instala twenty como global...

1
$ npm install -g twenty

...a continuación, puede obtener las ubicaciones de los servidores y las direcciones IP:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ 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 wikihtp.com
Ashburn, Virginia

Y ahí lo tiene, el servidor de Stack Abuse está ubicado en Asburn, Virginia. Interesante =)

Para obtener el código fuente completo, consulte el proyecto en Github. obinson/twentyjs).