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

发表:
大纲
  1. 1. 前言
  2. 2. 土著做法
  3. 3. 使用commander.js
    1. 3.1. 安装及使用
    2. 3.2. 常用api
      1. 3.2.1. version
      2. 3.2.2. option
      3. 3.2.3. command
      4. 3.2.4. description
      5. 3.2.5. action
      6. 3.2.6. parse
      7. 3.2.7. outputHelp
  4. 4. 实践

前言

在当下,作为一名前端码农,不知道Nodejs是不可原谅的。可以说,除了一些特别要求的业务范畴,常见的后端业务Nodejs都能handle住。

Nodejs的另一个常用场景是,造出一些实用工具,而这些工具大部分都是一些命令行程序。今天我们就来介绍如何写出一个Nodejs的命令行应用程序。

土著做法

当一个Nodejs程序运行时,会有许多存在内存中的全局变量,其中有一个叫做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好像是一个数组,其中第一个元素是node的执行路径,第二个元素是当前执行文件的路径,从第三个元素开始,是执行时带入的参数。

所以,规律很简单。我们在写命令行程序时,只需要对process.argv这个数组的第三个元素及其之后的参数进行解析即可。

如果不嫌麻烦,完全可以写出很多判断分支来做。但是现在我们有更好的方法。

使用commander.js

commander.jsTJ所写的一个工具包,其作用是让node命令行程序的制作更加简单。

安装及使用

安装很简单,

$ npm install commander

注意包名是commander而不是commander.js

然后我们在新建一个js文件,叫做index.js,内容如下

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)

此时,一个简单的命令行程序就完成了。我们通过如下的命令来执行它,

$ 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第一个优势就是提供了简介的api对可选项、参数进行解析。第二个优势就是自动生成帮助的文本信息。

常用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回调中处理逻辑。

version

用法: .version('x.y.z')

用于设置命令程序的版本号,

option

用户:.option('-n, --name <name>', 'your name', 'GK')

  • 第一个参数是选项定义,分为短定义和长定义。用|, 连接。
    • 参数可以用<>或者[]修饰,前者意为必须参数,后者意为可选参数。
  • 第二个参数为选项描述
  • 第三个参数为选项参数默认值,可选。

command

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

  • command的用法稍微复杂,原则上他可以接受三个参数,第一个为命令定义,第二个命令描述,第三个为命令辅助修饰对象。
  • 第一个参数中可以使用<>或者[]修饰命令参数
  • 第二个参数可选。
    • 当没有第二个参数时,commander.js将返回Command对象,若有第二个参数,将返回原型对象。
    • 当带有第二个参数,并且没有显示调用action(fn)时,则将会使用子命令模式。
    • 所谓子命令模式即,./pm./pm-install./pm-search等。这些子命令跟主命令在不同的文件中。
  • 第三个参数一般不用,它可以设置是否显示的使用子命令模式。

description

用法:.description('command description')

用于设置命令的描述

action

用法:.action(fn)

用于设置命令执行的相关回调。fn可以接受命令的参数为函数形参,顺序与command()中定义的顺序一致。

parse

用法:program.parse(process.argv)

此api一般是最后调用,用于解析process.argv

outputHelp

用法:program.outputHelp()

一般用于未录入参数时自动打印帮助信息。

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,作用是在执行目录下生成一个.npmrc文件,用使用默认的registry、disturl、loglevel配置(指向的是npm.taobao.org)。

首先我们得创建一个项目,

$ 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字段,然后执行npm adduser&npm publish,将这个工具包推送npmjs.org供所有人使用了。

因为我已经很长时间没更新博客,没有往npm仓库上推送package了,这次又踩了一次之前遇到的坑,泪崩。详见npm adduser的坑

如果对如何上传自己的工具包到npm还不是很清楚,可以参考这篇文章

这篇文章介绍除了介绍commander.js之外,还介绍了chalkprocess等在创建命令行工具时的常用工具,值得一读。