在當下,作為一名前端碼農(nóng),不知道Nodejs是不可原諒的??梢哉f,除了一些特別要求的業(yè)務范疇,常見的后端業(yè)務Nodejs都能handle住。
Nodejs的另一個常用場景是,造出一些實用工具,而這些工具大部分都是一些命令行程序。今天我們就來介紹如何寫出一個Nodejs的命令行應用程序。
當一個Nodejs程序運行時,會有許多存在內(nèi)存中的全局變量,其中有一個叫做process
,意為進程對象。process
對象中有一個叫做argv
的屬性。命令行程序的第一個重頭戲就是解析這個process.argv
屬性。
我們先隨便寫一個node程序,把process.argv
打印出來看看,
$ node test1.js --name gk
[
'/usr/local/Cellar/node/6.6.0/bin/node',
'/Users/gejiawen/code/20160921/test1.js',
'--name',
'gk'
]
看起來process.argv
好像是一個數(shù)組,其中第一個元素是node的執(zhí)行路徑,第二個元素是當前執(zhí)行文件的路徑,從第三個元素開始,是執(zhí)行時帶入的參數(shù)。
所以,規(guī)律很簡單。我們在寫命令行程序時,只需要對process.argv
這個數(shù)組的第三個元素及其之后的參數(shù)進行解析即可。
如果不嫌麻煩,完全可以寫出很多判斷分支來做。但是現(xiàn)在我們有更好的方法。
commander.js是TJ所寫的一個工具包,其作用是讓node命令行程序的制作更加簡單。
安裝很簡單,
$ npm install commander
注意包名是commander而不是commander.js。
然后我們在新建一個js文件,叫做index.js,內(nèi)容如下
var program = require('commander')
program
.version('0.0.1')
.description('a test cli program')
.option('-n, --name <name>', 'your name', 'GK')
.option('-a, --age <age>', 'your age', '22')
.option('-e, --enjoy [enjoy]')
program.parse(process.argv)
此時,一個簡單的命令行程序就完成了。我們通過如下的命令來執(zhí)行它,
$ node index.js -h
結果如下,
$ ./test -h
Usage: test [options]
a test cli program
Options:
-h, --help output usage information
-V, --version output the version number
-n, --name <name> your name
-a, --age <age> your age
-e, --enjoy [enjoy]
commander.js第一個優(yōu)勢就是提供了簡介的api對可選項、參數(shù)進行解析。第二個優(yōu)勢就是自動生成幫助的文本信息。
commander.js中命令行有兩種可變性,一個叫做option
,意為選項。一個叫做command
,意為命令。
看兩個例子,
program
.version('0.0.1')
.option('-C, --chdir <path>', 'change the working directory')
.option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
.option('-T, --no-tests', 'ignore test hook')
program
.command('setup')
.description('run remote setup commands')
.action(function() {
console.log('setup');
});
program
.command('exec <cmd>')
.description('run the given remote command')
.action(function(cmd) {
console.log('exec "%s"', cmd);
});
program
.command('teardown <dir> [otherDirs...]')
.description('run teardown commands')
.action(function(dir, otherDirs) {
console.log('dir "%s"', dir);
if (otherDirs) {
otherDirs.forEach(function (oDir) {
console.log('dir "%s"', oDir);
});
}
});
program
.command('*')
.description('deploy the given env')
.action(function(env) {
console.log('deploying "%s"', env);
});
program.parse(process.argv);
option
設置的選項可以通過program.chdir
或者program.noTests
來訪問。command
設置的命令通常在action
回調(diào)中處理邏輯。version
用法: .version('x.y.z')
用于設置命令程序的版本號,
option
用戶:.option('-n, --name <name>', 'your name', 'GK')
|
,,
,
連接。<>
或者[]
修飾,前者意為必須參數(shù),后者意為可選參數(shù)。command
用法:.command('init <path>', 'description')
command
的用法稍微復雜,原則上他可以接受三個參數(shù),第一個為命令定義,第二個命令描述,第三個為命令輔助修飾對象。<>
或者[]
修飾命令參數(shù)Command
對象,若有第二個參數(shù),將返回原型對象。action(fn)
時,則將會使用子命令模式。./pm
,./pm-install
,./pm-search
等。這些子命令跟主命令在不同的文件中。description
用法:.description('command description')
用于設置命令的描述
action
用法:.action(fn)
用于設置命令執(zhí)行的相關回調(diào)。fn
可以接受命令的參數(shù)為函數(shù)形參,順序與command()
中定義的順序一致。
parse
用法:program.parse(process.argv)
此api一般是最后調(diào)用,用于解析process.argv
。
outputHelp
用法:program.outputHelp()
一般用于未錄入?yún)?shù)時自動打印幫助信息。
eg:
if (!process.argv.slice(2).length) {
program.outputHelp(make_red);
}
function make_red(txt) {
return colors.red(txt); //display the help text in red on the console
}
下面我們來做一個小工具,實踐一下commander.js的強大之處。
這個工具叫做npmrc-local,作用是在執(zhí)行目錄下生成一個.npmrc
文件,用使用默認的registry、disturl、loglevel配置(指向的是npm.taobao.org)。
首先我們得創(chuàng)建一個項目,
$ mkdir npmrc-local
$ git init
$ npm init
$ touch .gitignore
$ touch bin/npmrc.js
$ touch lib/index.js
修改package.json
文件,添加bin
字段,
{
"bin": {
"npmrc": "./bin/npm.js"
}
}
修改bin/npmrc.js
,
#!/usr/bin/env node
require('../lib/index')
修改lib/index.js
,
#!/usr/bin/env node
var fs = require('fs')
var path = require('path')
var readline = require('readline')
var program = require('commander')
var rc = require('../rc')
var exit_bak = process.exit
program
.version('0.0.1')
.allowUnknownOption()
.option('-r, --registry <registry>', 'use custom registry', rc.registry)
.option('-d, --dist-url <disturl>', 'use custom dist url', rc.disturl)
.option('-l, --log-level <loglevel>', 'use custom log level', rc.loglevel)
program.parse(process.argv)
program.registry && (rc.registry = program.registry)
program.distUrl && (rc.disturl = program.distUrl)
program.logLevel && (rc.loglevel = program.logLevel)
if (!_exit.exited) {
_main()
}
// Graceful exit for async STDIO
function _exit(code) {
var draining = 0
var streams = [process.stdout, process.stderr]
function done() {
if (!(draining--)) {
exit_bak(code)
}
}
_exit.exited = true
streams.forEach(function (stream) {
draining += 1
stream.write('', done)
})
done()
}
function _confirm(msg, cb) {
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question(msg, function (input) {
rl.close()
cb(/^y|yes|ok|true$/i.test(input))
})
}
function _write(path, content, mode) {
fs.writeFileSync(path, content, {
mode: mode || 0o666
})
console.log('success!!!')
}
function _generateFile(filePath) {
var content = 'registry={registry}\ndisturl={disturl}\nloglevel={loglevel}\n'
content = content.replace(/\{(\w+)\}/gi, function (a, b) {
return rc[b]
})
_write(filePath, content)
}
function _overwrite(filePath) {
_generateFile(filePath)
}
function _existNpmRC(filePath) {
fs.exists(filePath, function (exists) {
if (exists) {
_confirm('ATTENTION: .npmrc is exist, over write? [y/N] ', function (ans) {
ans ? _overwrite(filePath) : console.log('bye!')
})
} else {
_generateFile(filePath)
}
})
}
function _main() {
var filePath = path.resolve(process.cwd(), '.npmrc')
console.log('writing path: ' + filePath)
_existNpmRC(filePath)
}
所有的代碼工作完畢之后,修改package.json
中version字段,然后執(zhí)行npm adduser
&npm publish
,將這個工具包推送npmjs.org供所有人使用了。
因為我已經(jīng)很長時間沒更新博客,沒有往npm倉庫上推送package了,這次又踩了一次之前遇到的坑,淚崩。詳見npm adduser的坑。
如果對如何上傳自己的工具包到npm還不是很清楚,可以參考這篇文章。
這篇文章介紹除了介紹commander.js之外,還介紹了chalk和process等在創(chuàng)建命令行工具時的常用工具,值得一讀。
更多建議: