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 *