使用nodejs寫一個能交互的命令行程序

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

相信很多前端都聽說過或者使用過Gulp, Angular CLI, Cordova, Yeoman或其他類似的命令行工具铺韧。但有想過這些程序是怎么實現(xiàn)的嗎诲侮?例如在Angular CLI中使用ng new <project-name>后會建立一個已經有基本配置的angular項目翩肌;又或者像Yeoman迂苛,也能運行時候輸入或者選擇配置項,讓用戶能夠自定義項目配置弃揽,快速搭建好開發(fā)時候需要用到的開發(fā)環(huán)境脯爪。下面的教程,就是講如何使用node寫一個像這樣的命令行工具矿微。

在這篇教程中痕慢,我們會開發(fā)一個命令行工具,用戶能夠輸入一個CSV文件地址涌矢,從而獲取到文件里面的用戶信息掖举,然后模擬群發(fā)郵件(原文是使用SendGrid Api模擬發(fā)送)
文章目錄:
1."Hello World"
2.處理命令行參數(shù)
3.運行時輸入參數(shù)
4.模擬發(fā)送郵件
5.改變輸出內容樣式
6.變成shell命令

“Hello World”

開始前,首先你得有node娜庇,如果沒有拇泛,請自行安裝下滨巴。node中自帶npm,使用npm能安裝許多開源的node模塊俺叭。首先,使用npm創(chuàng)建一個node項目

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

除這些參數(shù)外泰偿,npm還提供了其他如Git repository等參數(shù)熄守,可根據(jù)自身需求設置輸入。執(zhí)行完npm init后耗跛,會發(fā)現(xiàn)在同目錄下生成了一個package.json文件裕照,文件里面包含了上面命令輸入的信息。配置內容信息可以在package.json文檔中找到调塌。

然后晋南,還是從最簡單的Hello World入手。首先在同目錄下建一個broadcast.js文件

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

然后在terminal中執(zhí)行

$ node broadcast
Hello World!

well done, 根據(jù)package.json文檔羔砾,我們可以找到一個dependencies參數(shù)负间,在這參數(shù)中你可以找到所有這項目需要用到的第三方模塊和它們的版本號,上面也有提及到姜凄,我們需要用到模塊去開發(fā)這個工具政溃。最后開發(fā)完成,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的具體用處跟其他參數(shù)态秧,可以自行查看董虱。

處理命令行參數(shù)

node原生也有讀取命令行的函數(shù)process.argv,但是解析參數(shù)是個繁瑣的工作申鱼,所以我們會使用Commander去替代這些工作愤诱。Commande的另外一個好處就是不用額外的去寫一個--help函數(shù),只要定義了其他參數(shù)捐友,--help函數(shù)就會自動生成淫半。首先安裝一下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)

從上面可以看出,處理一個參數(shù)是十分簡單的楚殿。我們定義了一個--list的參數(shù)撮慨,現(xiàn)在我們就能通過--list參數(shù)獲取到命令行傳過來的值。在這程序中脆粥,list應該是接收一個csv的地址參數(shù)砌溺,然后打印在console中。

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

從js中可以看到還有一個version參數(shù)变隔,所以我們可以使用--version讀取版本號规伐。

$ node broadcast --version
0.0.1

又或者能使用--help獲取app能接收的參數(shù)

$ 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

現(xiàn)在我們已經能夠接收到命令行傳遞過來的參數(shù)了,下面我們會利用接收到的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

現(xiàn)在,更新下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方法把列數(shù)據(jù)解析為數(shù)組馅而,然后在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

運行時輸入參數(shù)

上面已經實現(xiàn)了獲取命令行參數(shù)祥诽,但如果想在運行時候接收參數(shù)值的話我們就需要另外一個模塊inquirer.js,通過這個模塊瓮恭,用戶可以自定義多種參數(shù)類型雄坪,如文本,密碼屯蹦,單選或者多選列表等维哈。

下面的demo會通過inquirer接收郵件發(fā)送人的名字,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數(shù)據(jù)
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方法接受一個數(shù)組參數(shù)笨农,數(shù)組里可以自定義運行時需要接受的問題參數(shù),在這demo里面帖渠,我們想知道發(fā)送者的名字還要email還有郵件主題谒亦,所以定義了一個questions的數(shù)組來儲存問題,從對象里面可以看到有一個input的參數(shù)空郊,除此外還可以接受password等其他類型份招,具體可以查詢一下inquirer的文檔。此外狞甚,參數(shù)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' }

模擬發(fā)送郵件

由于原文使用的sendgrid沒有跑通,所以只組裝了一下數(shù)據(jù)模擬了發(fā)送郵件涩盾。原本的第五部分也在這里一起用上了十气。

// 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數(shù)據(jù)
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)執(zhí)行芹枷,還需要做以下操作衅疙。首先,添加shebang在js的頭部鸳慈,讓shell知道如何執(zhí)行這個文件饱溢。

#!/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的參數(shù),這個參數(shù)可以使broadcast命令與broadcast.js建立連接蝶涩。

最后一步理朋,在全局安裝一下依賴包。在項目目錄運行一下下面的命令绿聘。

$ npm install -g

然后測試一下命令

$ broadcast --help

需要注意的是,在開發(fā)時候如果使用commaner默認給出的命令執(zhí)行broadcast則在代碼中所做的任何更改都是看不見的次舌。假如輸入which broadcast熄攘,你會發(fā)現(xiàn)地址不是你當前目錄,所以這時應該要用npm link去查看命令的目錄映射彼念。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末挪圾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子逐沙,更是在濱河造成了極大的恐慌哲思,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吩案,死亡現(xiàn)場離奇詭異棚赔,居然都是意外死亡,警方通過查閱死者的電腦和手機徘郭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門靠益,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人残揉,你說我怎么就攤上這事胧后。” “怎么了抱环?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵壳快,是天一觀的道長。 經常有香客問我镇草,道長眶痰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任陶夜,我火速辦了婚禮凛驮,結果婚禮上,老公的妹妹穿的比我還像新娘条辟。我一直安慰自己黔夭,他們只是感情好宏胯,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著本姥,像睡著了一般肩袍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上婚惫,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天氛赐,我揣著相機與錄音,去河邊找鬼先舷。 笑死艰管,一個胖子當著我的面吹牛,可吹牛的內容都是我干的蒋川。 我是一名探鬼主播牲芋,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼捺球!你這毒婦竟也來了缸浦?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤氮兵,失蹤者是張志新(化名)和其女友劉穎裂逐,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泣栈,經...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡卜高,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了秩霍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片篙悯。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖铃绒,靈堂內的尸體忽然破棺而出鸽照,到底是詐尸還是另有隱情,我是刑警寧澤颠悬,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布矮燎,位于F島的核電站,受9級特大地震影響赔癌,放射性物質發(fā)生泄漏诞外。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一灾票、第九天 我趴在偏房一處隱蔽的房頂上張望峡谊。 院中可真熱鬧,春花似錦、人聲如沸既们。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽啥纸。三九已至号杏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間斯棒,已是汗流浹背盾致。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留荣暮,地道東北人庭惜。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像穗酥,于是被迫代替她去往敵國和親胀蛮。 傳聞我的和親對象是個殘疾皇子裕便,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內容

  • JavaScript 模塊化編程 網站越來越復雜通贞,js代碼取具、js文件也越來越多实抡,會遇到什么問題脂矫? 命名沖突吏垮; 文件...
    magic_pill閱讀 1,411評論 0 1
  • 前言 js是從網頁小腳本演變過來的捐腿,至今课锌,前端的js庫厨内,也不像一個真正的模塊。前端js經歷了工具類庫渺贤、組件庫雏胃、前端...
    白昔月閱讀 3,271評論 2 11
  • Node.js是目前非常火熱的技術志鞍,但是它的誕生經歷卻很奇特瞭亮。 眾所周知,在Netscape設計出JavaScri...
    w_zhuan閱讀 3,609評論 2 41
  • 什么是 NPM npm之于Node固棚,就像pip之于Python,gem之于Ruby,composer之于PHP统翩。 ...
    ihoey閱讀 6,249評論 2 36
  • 又看到李子柒最近發(fā)的兩個視頻,看到評論里有一個人說:她就是想要的詩和遠方此洲。 想把視頻再一次分享到朋友圈厂汗,可是思考了...
    刺殼兒閱讀 303評論 0 0