使用nodejs写一个能交互的命令行程序

原文译自Smashing Magazine -- How To Develop An Interactive Command Line Application Using Node.js

相信很多前端都听说过或者使用过Gulp, Angular CLI, Cordova, Yeoman或其他类似的命令行工具。但有想过这些程序是怎么实现的吗?例如在Angular CLI中使用ng new <project-name>后会建立一个已经有基本配置的angular项目;又或者像Yeoman,也能运行时候输入或者选择配置项,让用户能够自定义项目配置,快速搭建好开发时候需要用到的开发环境。下面的教程,就是讲如何使用node写一个像这样的命令行工具。

在这篇教程中,我们会开发一个命令行工具,用户能够输入一个CSV文件地址,从而获取到文件里面的用户信息,然后模拟群发邮件(原文是使用SendGrid Api模拟发送)
文章目录:
1."Hello World"
2.处理命令行参数
3.运行时输入参数
4.模拟发送邮件
5.改变输出内容样式
6.变成shell命令

“Hello World”

开始前,首先你得有node,如果没有,请自行安装下。node中自带npm,使用npm能安装许多开源的node模块。首先,使用npm创建一个node项目

$ npm init
name: broadcast
version: 0.0.1
description: CLI utility to broadcast email
entry point: broadcast.js

除这些参数外,npm还提供了其他如Git repository等参数,可根据自身需求设置输入。执行完npm init后,会发现在同目录下生成了一个package.json文件,文件里面包含了上面命令输入的信息。配置内容信息可以在package.json文档中找到。

然后,还是从最简单的Hello World入手。首先在同目录下建一个broadcast.js文件

// broadcast.js
console.log('Hello World!')

然后在terminal中执行

$ node broadcast
Hello World!

well done, 根据package.json文档,我们可以找到一个dependencies参数,在这参数中你可以找到所有这项目需要用到的第三方模块和它们的版本号,上面也有提及到,我们需要用到模块去开发这个工具。最后开发完成,package.json应该如下

{
    "name": "broadcast",
    "version": "0.0.1",
    "description": "CLI utility to broadcast emails",
    "main": "broadcast.js",
    "license": "MIT",
    "dependencies": {
        "chalk": "^1.1.3",
        "commander": "^2.9.0",
        "csv": "^1.1.0",
        "inquirer": "^2.0.0"
    }
}

这几个模块 Chalk, Commander, Inquirer, CSV的具体用处跟其他参数,可以自行查看。

处理命令行参数

node原生也有读取命令行的函数process.argv,但是解析参数是个繁琐的工作,所以我们会使用Commander去替代这些工作。Commande的另外一个好处就是不用额外的去写一个--help函数,只要定义了其他参数,--help函数就会自动生成。首先安装一下Commander和其他package

$ npm install commander chalk csv inquirer --save

然后修改broadcast.js

// broadcast
const program = require('commander')

program
    .version('0.0.1')
    .option('-l, --list [list]', 'list of customers in CSV file')
    .parse(process.argv)

console.log(program.list)

从上面可以看出,处理一个参数是十分简单的。我们定义了一个--list的参数,现在我们就能通过--list参数获取到命令行传过来的值。在这程序中,list应该是接收一个csv的地址参数,然后打印在console中。

$ node broadcast --list ./test.csv
./test.csv

从js中可以看到还有一个version参数,所以我们可以使用--version读取版本号。

$ node broadcast --version
0.0.1

又或者能使用--help获取app能接收的参数

$ node broadcast --help

  Usage: broadcast [options]

  Options:

    -h, --help                 output usage information
    -V, --version              output the version number
    -l, --list <list>          list of customers in CSV file

现在我们已经能够接收到命令行传递过来的参数了,下面我们会利用接收到的CSV文件地址,并使用CSV模块处理CSV文件的内容。
我们会使用下面的比哦啊哥内容作为CSV文件的内容。使用CSV模块,会读取内容,并显示各列的内容。

First name Last name Email
Dwight Schrute dwight.schrute@dundermifflin.com
Jim Halpert jim.halpert@dundermifflin.com
Pam Beesly pam.beesly@dundermifflin.com
Ryan Howard ryan.howard@dundermifflin.com
Stanley Hudson stanley.hudson@dundermifflin.com

现在,更新下broadcast.js,使用CSV读取内容并打印在console

// broadcast.js
const program = require('commander')
const csv = require('csv')
const fs = require('fs')

program
    .version('0.0.1')
    .option('-l, --list [list]', 'List of customers in CSV')
    .parse(process.argv)

const stream = fs.createReadStream(program.list)
stream
    .pipe(csv.parse({ delimiter : "," }))
    .on('data', function(data) {
         const firstname = data[0]
         const lastname = data[1]
         const email = data[2]

         console.log(firstname, lastname, email)
    })

除csv模块外,还使用了node的File System模块读取文件内容,csv的parse方法把列数据解析为数组,然后在terminal中运行一下命令

$ node broadcast.js --list ./test.csv
Dwight Schrute dwight.schrute@dundermifflin.com
Jim Halpert jim.halpert@dundermifflin.com
Pam Beesly pam.beesly@dundermifflin.com
Ryan Howard ryan.howard@dundermifflin.com
Stanley Hudson stanley.hudson@dundermifflin.com

运行时输入参数

上面已经实现了获取命令行参数,但如果想在运行时候接收参数值的话我们就需要另外一个模块inquirer.js,通过这个模块,用户可以自定义多种参数类型,如文本,密码,单选或者多选列表等。

下面的demo会通过inquirer接收邮件发送人的名字,email还有邮件主题。

// broadcast.js
...
const inquirer = require('inquirer')
const questions = [
  {
    type : "input",
    name : "sender.email",
    message : "Sender's email address - "
  },
  {
    type : "input",
    name : "sender.name",
    message : "Sender's name - "
  },
  {
    type : "input",
    name : "subject",
    message : "Subject - "
  }
]

program
  .version('0.0.1')
  .option('-l, --list [list]', 'List of customers in CSV')
  .parse(process.argv)

// 储存CSV数据
const contactList = []
const stream = fs.createReadStream(program.list)
    .pipe(csv.parse({ delimiter : "," }))

stream
  .on('error', function (err) {
    return console.error(err.message)
  })
  .on('data', function (data) {
    let name = data[0] + " " + data[1]
    let email = data[2]
    contactList.push({ name : name, email : email })
  })
  .on('end', function () {
    inquirer.prompt(questions).then(function (answers) {
      console.log(answers)
    })
  })

Inquire.js的prompt方法接受一个数组参数,数组里可以自定义运行时需要接受的问题参数,在这demo里面,我们想知道发送者的名字还要email还有邮件主题,所以定义了一个questions的数组来储存问题,从对象里面可以看到有一个input的参数,除此外还可以接受password等其他类型,具体可以查询一下inquirer的文档。此外,参数name保存input的key值。prompt方法会返还一个promise对象,promise中会返回一个answer变量,里面带有刚才输入的值。

$ node broadcast -l input/employees.csv
? Sender's email address -  kitssang_demo@163.com
? Sender's name -  kit
? Subject - Hello World
{ sender:
   { email: '  kitssang_demo@163.com',
     name: 'kit' },
  subject: 'Hello World' }

模拟发送邮件

由于原文使用的sendgrid没有跑通,所以只组装了一下数据模拟了发送邮件。原本的第五部分也在这里一起用上了。

// broadcast.js
...
program
    .version('0.0.1')
    .option('-l, --list [list]', 'list of customers in CSV file')
    .parse(process.argv)

const sendEmail = function(to, from, subject) {
    const sender = chalk.green(`${from.name}(${from.email})`)
    const receiver = chalk.green(`${to.name}(${to.email})`)
    const theme = chalk.blue(subject)
    
    console.log(`${sender} send a mail to ${receiver} and the subject of the email is ${theme}`)
}

// 储存CSV数据
let concatList = []
const stream = fs.createReadStream(program.list)
  .pipe(csv.parse({
    delimiter: ','
  }))
  .on('data', function(data) {
    const name = data[0] + ' ' + data[1]
    const email = data[2]

    concatList.push({
      name: name,
      email: email
    })
  })
  .on('end', function() {
    inquirer.prompt(questions).then((ans) => {
      for (let i = 0; i < concatList.length; i++) {
        sendEmail(concatList[i], ans.sender, ans.subject)
      }
    }).catch((err) => {
      console.log(err)
    })
  })

由于没有异步请求,async模块没有用上,另外使用了chalk模块改变了console打印结果的颜色。

变成shell命令

至此,整个工具已经基本完成,但是如果想像一个普通的shell命令(不加$ node xx)执行,还需要做以下操作。首先,添加shebang在js的头部,让shell知道如何执行这个文件。

#!/usr/bin/env node

// broadcast.js
const program = require("commander")
const inquirer = require("inquirer")
...

然后再配置一下package.json使代码可运行

…
  "description": "CLI utility to broadcast emails",
  "main": "broadcast.js",
  "bin" : {
    "broadcast" : "./broadcast.js"
  }
…

从代码可以看到加了一个bin的参数,这个参数可以使broadcast命令与broadcast.js建立连接。

最后一步,在全局安装一下依赖包。在项目目录运行一下下面的命令。

$ npm install -g

然后测试一下命令

$ broadcast --help

需要注意的是,在开发时候如果使用commaner默认给出的命令执行broadcast则在代码中所做的任何更改都是看不见的。假如输入which broadcast,你会发现地址不是你当前目录,所以这时应该要用npm link去查看命令的目录映射。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • JavaScript 模块化编程 网站越来越复杂,js代码、js文件也越来越多,会遇到什么问题? 命名冲突; 文件...
    magic_pill阅读 1,408评论 0 1
  • 前言 js是从网页小脚本演变过来的,至今,前端的js库,也不像一个真正的模块。前端js经历了工具类库、组件库、前端...
    白昔月阅读 3,269评论 2 11
  • Node.js是目前非常火热的技术,但是它的诞生经历却很奇特。 众所周知,在Netscape设计出JavaScri...
    w_zhuan阅读 3,607评论 2 41
  • 什么是 NPM npm之于Node,就像pip之于Python,gem之于Ruby,composer之于PHP。 ...
    ihoey阅读 6,247评论 2 36
  • 又看到李子柒最近发的两个视频,看到评论里有一个人说:她就是想要的诗和远方。 想把视频再一次分享到朋友圈,可是思考了...
    刺壳儿阅读 302评论 0 0