使用commander.js做一個Nodejs命令行程序

2018-06-09 17:32 更新

前言

在當下,作為一名前端碼農(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

commander.jsTJ所寫的一個工具包,其作用是讓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)勢就是自動生成幫助的文本信息。

常用api

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ù)可以用<>或者[]修飾,前者意為必須參數(shù),后者意為可選參數(shù)。
  • 第二個參數(shù)為選項描述
  • 第三個參數(shù)為選項參數(shù)默認值,可選。

command

用法:.command('init <path>', 'description')

  • command的用法稍微復雜,原則上他可以接受三個參數(shù),第一個為命令定義,第二個命令描述,第三個為命令輔助修飾對象。
  • 第一個參數(shù)中可以使用<>或者[]修飾命令參數(shù)
  • 第二個參數(shù)可選。
    • 當沒有第二個參數(shù)時,commander.js將返回Command對象,若有第二個參數(shù),將返回原型對象。
    • 當帶有第二個參數(shù),并且沒有顯示調(diào)用action(fn)時,則將會使用子命令模式。
    • 所謂子命令模式即,./pm,./pm-install,./pm-search等。這些子命令跟主命令在不同的文件中。
  • 第三個參數(shù)一般不用,它可以設置是否顯示的使用子命令模式。

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之外,還介紹了chalkprocess等在創(chuàng)建命令行工具時的常用工具,值得一讀。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號