30 分钟使用 Node 实现一个命令行程序
让我很无奈的是使用Java编写命令行程序是比较麻烦的,好在NodeJs干这事很方便, 在接下来的30分钟里我将教你编写一个有趣的终端程序并将它发布到npm仓库中,赶紧GET吧~
我实在想不到起什么名字了,就叫 lowb 吧。。。我们实现好的程序是这样的:
我把源码放在:https://github.com/biezhi/lowb
如何录制终端命令,我写了一篇文章 教你使用asciinema录制命令行操作
在开始之前请务必确认你安装了Node的环境,我目前使用的NodeJs环境是 v6.10.3。
创建项目结构
mkdir lowb && cd lowb && npm init
根据提示初始化你的项目,如果你不懂怎么填写一路回车也是可以的(终端会提示你输入一些项目信息)
创建一个二进制目录
mkdir bin
在 lowb 根目录下创建一个 bin 目录,我们将可执行文件存放在这里,此时你的项目结构是这样的:
⬢  lowb
.
├── bin
└── package.json
1 directory, 1 file
光杆司令的做法
原理是解析Node中的进程对象 process.argv
首先在 bin 目录下创建一个文件 lowb.js 作为我们的运行程序
#!/usr/bin/env node
var fs = require("fs"), path = process.cwd();
var appInfo = require('../package.json');
function app(obj) {
    if(obj[0] === '-v' || obj[0] === '--version'){
        console.log('  version is ' + appInfo.version);
    } else if(obj[0] === '-h' || obj[0] === '--help'){
        console.log('Useage:');
        console.log('  -v --version [show version]');
    } else{
        fs.readdir(path, function(err, files){
            if(err){
                return console.log(err);
            }
            for(var i = 0; i < files.length; i += 1){
                console.log(files[i]);
            }
        });
    }
};
//获取除第一个命令以后的参数,使用空格拆分
app(process.argv.slice(2));
这段代码中非常简单,我们首先引入了 fs 模块和 package.json 的变量(用于获取版本号)。 然后编写了一个函数进行调用,里面只实现了一个 -v 和 -h 的命令。
process.argv是一个数组,第一个元素返回node执行路径,第二个元素是当前执行文件的路径, 从第三个开始是运行时带的参数
指定执行脚本
修改一下 package.json
{
  "name": "lowb",
  "version": "1.0.0",
  "description": "lowb项目,在命令行下输出名言、段子、诗歌的小玩意~",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/biezhi/lowb.git"
  },
  "keywords": [
    "lowb"
  ],
  "bin": {
    "lowb": "bin/lowb.js"
  },
  "author": "biezhi <biezhi.me@gmail.com>",
  "license": "MIT"
}
注意添加了 bin 这个参数,将 lowb 这个命令映射到 bin/lowb.js 这个文件, 我们打包安装后调用 lowb 命令会执行 bin/lowb.js 中的内容。
安装到本地
sudo npm install . -g
在 lowb 根目录下执行如上命令,你将看到类似如下的输出
⬢  lowb  sudo npm install . -g
Password:
/Users/biezhi/.nvm/versions/node/v6.10.3/bin/lowb
    -> /Users/biezhi/.nvm/versions/node/v6.10.3/lib/node_modules/lowb/bin/lowb.js
/Users/biezhi/.nvm/versions/node/v6.10.3/lib
此时已经将命令安装到本地了,我们可以试试在终端下运行了:
⬢  lowb  lowb
bin
package.json
⬢  lowb  lowb -v
version is 1.0.0
⬢  lowb  lowb -h
Useage:
  -v --version [show version]
⬢  lowb  lowb --help
bin
package.json
试试Commander.js
前面这种做法当然是可以完成一个命令行程序的,但这样的做法就像是在刀耕火种的时代,而且功能有限; 在前端界赫赫有名的工程师 tj 写了一个库 commander.js 就是帮助我们简化命令行程序开发。
它是受Ruby中的一个库 commander 而诞生,是一款轻量级表现力强大的命令行框架。
我们来使用这个工具完成今天要做的小玩意。
安装Commander.js
npm install commander --save
快速入门
var program = require('commander');
program
  .version('0.1.0')
  .option('-p, --peppers', 'Add peppers')
  .option('-P, --pineapple', 'Add pineapple')
  .option('-b, --bbq-sauce', 'Add bbq sauce')
  .option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
  .parse(process.argv);
console.log('you ordered a pizza with:');
if (program.peppers) console.log('  - peppers');
if (program.pineapple) console.log('  - pineapple');
if (program.bbqSauce) console.log('  - bbq');
console.log('  - %s cheese', program.cheese);
这是官方README中的一段代码,我们写一个JS运行一下 cmd -h 看看
Usage: cmd [options]
  Options:
    -V, --version        output the version number
    -p, --peppers        Add peppers
    -P, --pineapple      Add pineapple
    -b, --bbq-sauce      Add bbq sauce
    -c, --cheese [type]  Add the specified type of cheese [marble]
    -h, --help           output usage information
常用api
commander.js 中命令行有两种可变性,option:选项,command:命令。
- 通过
option设置的选项可以通过program.chdir或者program.noTests来访问。 - 通过
command设置的命令通常在action回调中处理逻辑。 
本文中没有用到command就不详述了。
开始干它一票吧
我们希望做出来的使用效果是这样的:
lowb --help
  Usage: lowb [options]
  Options:
    -V, --version    output the version number
    -i, --index <n>  ascii art index, default is random
    -t, --type <value>  quotes/jokes/tang/song
    -h, --help       output usage information
-V, --version: 输出程序的版本号-i, --index <n>: ascii动物的索引,默认是随机的-t, --type <value>: 输出文本的类型,名言、段子、唐诗、宋词,默认是名言-h, --help: 帮助信息
引入 commander
var cmd     = require('commander');
编写命令处理的代码
cmd.version(appInfo.version)
    .option('-i, --index <n>', 'ascii art index, default is random', -1, parseInt)
    .option('-t, --type <value>', '[quotes|jokes|tang|song]', 'quotes', /^(quotes|jokes|tang|song)$/i)
    .on('--help', function(){
        console.log('\t' + appInfo.repository.url);
    }).parse(process.argv);
option 的常用API
- 第一个参数是选项定义,分为短定义和长定义,用 
|,,,连接,参数可以用<>或者[]修饰,前者意为必须参数,后者意为可选参数 - 第二个参数为选项描述
 - 第三个参数为选项参数默认值,可选
 
准备原材料
什么原材料?我们需要输出ascii的动物图像和一些名言、段子等文本,这里数据我就存放在 data 目录下。 当然做的高级点你可以用爬虫~ 本文引砖抛玉了
我准备了 animals.txt 里面存放的是ascii的动物图像,像这样:

如果觉得这些你不喜欢可以上 这里 看看还有更多的小动物任你把玩。
将这5个文本数据存放在 data 目录下,使用JS变量存储一下
var fs      = require('fs');
var path    = require('path');
var animals = fs.readFileSync(path.join(__dirname, '../data/animals.txt')).toString()
                .split('===============++++SEPERATOR++++====================\n');
var jokes  = fs.readFileSync(path.join(__dirname, '../data/jokes.txt')).toString().split('%\n');
var quotes  = fs.readFileSync(path.join(__dirname, '../data/quotes.txt')).toString().split('%\n');
var tang300 = fs.readFileSync(path.join(__dirname, '../data/tang300.txt')).toString().split('%\n');
var song100 = fs.readFileSync(path.join(__dirname, '../data/song100.txt')).toString().split('%\n');
读取数据
编写2个函数,一个用于产生随机的ascii动物文本,一个用于返回名言或段子
/**
 * 返回一个随机的动物ascii
 *
 * @returns {*}
 */
function randomAnimal() {
    return animals[Math.floor(Math.random() * animals.length)];
}
/**
 * 根据类型返回名言或段子
 *
 * @param type
 * @returns {string}
 */
function prefix(type) {
    switch (type) {
        case 'quotes':
            return quotes[Math.floor(Math.random() * quotes.length)];
        case 'jokes':
            return jokes[Math.floor(Math.random() * jokes.length)];
        case 'tang':
            return tang300[Math.floor(Math.random() * tang300.length)];
        case 'song':
            return song100[Math.floor(Math.random() * song100.length)];
        default:
            return tang300[Math.floor(Math.random() * tang300.length)];
    }
}
然后在命令解析的下面开始调用他们吧~
var animal = cmd.index === -1 ? randomAnimal() : animals[cmd.index];
console.log(prefix(cmd.type));
console.log(animal);
这里的逻辑非常简单,如果没有指定动物索引则随机获取一个小动物,否则取指定的。 然后输出名言/段子,输出一个小动物~ 大功告成
来试试吧
程序编写ok后我们安装到本地,然后执行 lowb -t jokes 试试看?
    阿里小米皆自主,百度排名最公平;
    京东全网最低价,当当爱国很理性;
    用户体验看新浪,网易从来少愤青;
    豆瓣从来不约炮,人人分享高水平;
    从不抄袭数腾讯, 开放安全三六零。
                                  ____
           ,.-''''-,__,..---'''```    ``''-.
          //      '   `.                    `,
         7;                         )         .
        Y    \  /                  /           L,
        : \. \\|                 ,`            | `'.
    ,.-'^, \\``',    (           ;             ;    `,
   //`_),.\|\)_ .\   /          ,A          ._,^      \
   L\)  ,+`[  e\ \.-`''--......-__`.  _,       `\.     )Y
   _,--`   \   )`.`,           // ````          / )_.-' |
  //,/`_)'` `''-. `/    _,.......----------'"""'``      /
  \\)\)          `"   +`  ________                   _,`
   `` `             ,`,'``         ```````'"""""""'``
                    |7  sk
                     \_,
默认输出的是名言,我加了-t参数指定来个段子。
发布到NPM仓库
我们希望所有人都可以下载到自己写的程序就需要将它发布到npm的仓库中, 发布前需要注册npm账号,顺手绑定一下你的github账号吧。注册ok后在项目根目录下执行 sudo npm publish 会提示你输入一下npm的账号和密码验证,完了就会推送到 npmjs.org 了,你可以搜到自己的程序。
像安装其他程序一样安装 sudo npm install lowb -g(记得把本地安装的卸载了)
通过这篇文章相信你也可以编写一个终端程序玩了~ (๑•̀ㅂ•́) ✧加油