Как создать приложение CLI на Node.js

Одна из моих самых любимых вещей в Node - это то, насколько легко создавать простые инструменты интерфейса командной строки (CLI). Между синтаксическим анализом аргументов с помощью yargs [https://www.npmjs.com/package/yargs] и управлением инструментами с помощью npm, Node просто упрощает это. Вот некоторые примеры инструментов, о которых я говорю: * forever [https://www.npmjs.com/package/forever] * uglifyjs [https://www.npmjs.com/package/uglifyjs] * is -up-cli [https://www.npmjs.com/package/is-up-cli] * jshint [https://www.npmjs.com/pac

Одна из моих самых любимых вещей в Node - это то, насколько легко создавать простые инструменты интерфейса командной строки (CLI). Между синтаксическим анализом аргументов с помощью yargs и управлением инструментами с помощью npm Node просто упрощает задачу.

Вот несколько примеров инструментов, о которых я говорю:

После установки (с параметром -g ) эти пакеты можно запускать из любой точки командной строки и работать так же, как встроенные инструменты Unix.

В последнее время я создал несколько приложений Node.js для командной строки и подумал, что было бы полезно написать об этом сообщение, которое поможет вам начать работу. Поэтому в этой статье я покажу вам, как создать инструмент командной строки для получения данных о местоположении для IP-адресов и URL-адресов.

Если вы видели статью Stack Abuse об изучении Node.js , вы, возможно, помните, что мы создали пакет под названием twenty который имел аналогичную функциональность. Мы будем развивать этот проект и превращать его в подходящий инструмент CLI с большей функциональностью.

Настройка проекта

Начнем с создания нового каталога и настройки проекта с помощью npm:

 $ mkdir twenty 
 $ npm init 

Нажмите Enter для всех запросов в последней команде, и у вас должен быть файл package.json

Обратите внимание, что, поскольку я уже взял имя пакета twenty на npm, вам придется переименовать его во что-то другое, если вы действительно хотите опубликовать. Или вы также можете охватить свой проект.

Затем создайте файл index.js

 $ touch index.js 

Это все, что нам действительно нужно для начала работы, и мы будем добавлять в проект по мере продвижения.

Разбор аргументов

Большинство приложений CLI принимают аргументы от пользователя, что является наиболее распространенным способом ввода данных. В большинстве случаев разобрать аргументы не так уж сложно, поскольку обычно используется лишь несколько команд и флагов. Но по мере того, как инструмент становится более сложным, будет добавляться больше флагов и команд, а синтаксический анализ аргументов может стать на удивление трудным.

Чтобы помочь нам в этом, мы будем использовать пакет под названием yargs , который является преемником популярного пакета optimist.

yargs был создан, чтобы помочь вам анализировать команды пользователя, например:

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

Теперь можно легко получить доступ к сложным optstrings, таким как node index.js install -v --a=22 -cde -x derp

 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 даже поможет вам с указанием командного интерфейса, поэтому, если ввод пользователя не соответствует определенным требованиям, он покажет ему сообщение об ошибке. Так, например, мы можем сказать yargs нам нужны как минимум 2 аргумента:

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

И если пользователь не предоставит хотя бы два, он увидит это сообщение об ошибке по умолчанию:

 $ node index.js foo 
 
 Not enough non-option arguments: got 1, need at least 2 

yargs - это гораздо больше, чем просто это, поэтому ознакомьтесь с readme для получения дополнительной информации.

Для twenty мы будем принимать несколько необязательных аргументов, таких как IP-адрес и некоторые флаги. На данный момент мы будем использовать такие yargs

 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; 

Поскольку ни один из наших аргументов не требуется, мы не будем использовать .demand() , но мы используем .alias() , который сообщает yargs что пользователь может использовать краткую или длинную форму каждого флага. Мы также добавили некоторую справочную документацию, чтобы показать пользователю, когда она ему понадобится.

Структурирование приложения

Теперь, когда мы можем получить ввод от пользователя, как нам взять этот ввод и преобразовать его в команду с необязательными аргументами? Есть несколько модулей, которые помогут вам в этом, в том числе:

Во многих из этих фреймворков синтаксический анализ аргументов фактически выполняется за вас, поэтому вам даже не нужно использовать yargs . А в commander большая часть его функциональных возможностей очень похожа на yargs , хотя он предоставляет способы маршрутизации команд к функциям.

Поскольку наше приложение довольно простое, мы пока будем использовать yargs .

Добавление кода

Мы не будем тратить здесь много времени, поскольку это относится только к нашему приложению CLI, но вот код, специфичный для нашего приложения:

 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); 
 }); 
 }); 
 }; 

Полный исходный код вы можете найти здесь .

Единственное, что нам осталось сделать с кодом, - это связать аргументы CLI с приведенным выше кодом приложения. Чтобы упростить задачу, мы поместим все это в функцию cli() , которую мы будем использовать позже.

Инкапсуляция синтаксического анализа аргументов и сопоставления команд в cli() помогает разделить код приложения, что позволяет импортировать этот код как библиотеку с помощью 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; 

Здесь вы можете видеть, что мы в основном просто используем if...else чтобы определить, какую команду запускать. Вы могли бы стать намного интереснее и использовать Flatiron для сопоставления строк регулярных выражений с командами, но это немного излишне для того, что мы здесь делаем.

Делаем его исполняемым

Чтобы мы могли запустить приложение, нам нужно указать несколько вещей в нашем package.json , например, где находится исполняемый файл. Но сначала давайте создадим исполняемый файл и его код. Создайте файл с именем twenty в каталоге twenty/bin/ и добавьте к нему:

 #!/usr/bin/env node 
 require('../index').cli(); 

Shebang ( #!/usr/bin/env node ) сообщает Unix, как выполнять файл, позволяя нам не node префикс узла. Вторая строка просто загружает код сверху и вызывает функцию cli()

В package.json добавьте следующий JSON:

 "bin": { 
 "twenty": "./bin/twenty" 
 } 

Это просто сообщает npm, где найти исполняемый файл при установке пакета с -g (global).

Итак, теперь, если вы установите twenty как глобальный ...

 $ npm install -g twenty 

... затем вы можете получить расположение серверов и 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 stackabuse.com 
 Ashburn, Virginia 

И вот, сервер Stack Abuse расположен в Асберне, штат Вирджиния. Интересно =)

Полный исходный код можно найти в проекте на Github .

comments powered by Disqus