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

    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

    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 commanderes 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.

     

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *