关闭

使用Node.js创建命令行工具

标签: atlassianBitBucket
7995人阅读 评论(0) 收藏 举报

在我的职业生涯中我已经写了数百种Bash脚本,但在Bash方面仍然有很多不足。每次我都要为一些简单的逻辑结构去查阅语法。如果我想使用curl或sed做些特技,我还不得不去查找操作说明。我在我的正则表达式中花费了几小时的蛮力进行单及双引号间可能的组合以及每一字符的转义和双转义,直至我得到了一些看上去像ASCII图形的东西, 同时努力记住 grep和perl正则表达式之间的区别。

 

于是,有一天,我看着在过去的十年里,每天都在使用的语言的最后六个字母,突然脑洞大开。原来您可以使用JavaScript...来编写脚本

 

在本教程中,我将把使用Node.js和npm创建一个脚本或命令行工具的技巧分享给您。主要包括以下方面:

l  使用npm打包新的shell命令

l  解析命令行选项

l  从标准输入(stdin)读取文本和口令

l  使用ES6生成器避开回调函数

l  错误输出及代码

l  染色终端输出

l  呈现一ASCII进度条

 

我很喜欢使用工作的例子,因此为了说明这些概念,我们将创建一个命名为snippet的shell命令,它可以从本地磁盘上的文件创建一个Bitbucket  Snippet。

这就是我们的目标:



打包shell 命令

npm不仅仅是为了管理您的apps和网页的依赖关系。您还可以用它打包和分配新的shell命令。


第一步是使用npm init创建一新的npm项目:

$ npm init
name: bitbucket-snippet
version: 0.0.1
description: A command-line tool for creating Bitbucket snippets.
entry point: index.js
license: Apache-2.0


这将为我们的项目生成一个新的package.json文件。然后我们需要创建一个含有我们脚本的JS文件。 让我们按照Node.js管理将其叫做index.js。

+ #!/usr/bin/env node
+ console.log('Hello, world!');


请注意我们必须添加了一个 shebang 告诉我们的shell如何调用该脚本。


下一步我们需要添加一个bin节点至package.json的最顶层。在我们的shell中,属性键(片断,在我们的例子中)将成为用户调用的命令。 属性值是相对于package.json的脚本路径。

...
  "author": "Tim Pettersen",
  "license": "Apache-2.0",
+ "bin": {
+   "snippet": "./index.js"
+ }
}


现在我们有了一个工作shell 命令!让我们安装并对其测试。

$ npm install -g
$ snippet
Hello, world!


灵活地! npm install -g 的确将将脚本和我们路径上的位置连接起来,因此我们可以像任何其他shell命令一样使用它。

$ which snippet
/usr/local/bin/snippet
$ readlink /usr/local/bin/snippet
../lib/node_modules/bitbucket-snippet/index.js


在开发过程中,我们可以非常方便地使用 npm link 制作一个符号链接 (symlink) 至我们正在编写脚本的index.js的路径上。

$ npm link
/usr/local/bin/snippet -> /usr/local/lib/node_modules/bitbucket-snippet/index.js
/usr/local/lib/node_modules/bitbucket-snippet -> /Users/kannonboy/src/bitbucket-snippet


当我们准备好时,我们可以使用npm publish向公共npm 注册表发布我们的脚本。世界上的所有人都可以通过以下操作将其安装在自己的机器上:

$ npm install -g bitbucket-snippet


但是首先让我们的脚本工作起来!


语法分析命令行选项

我们的脚本将需要一些来自用户的输入: 他们的 Bitbucket 用户名, 他们的口令以及上传作为代码片段的文件。脚本典型模式是遍历这些值作为命令参数。


您可以使用process.argv 来获取调用node脚本的参数,但是有一些npm包可以为您的语法分析参数和选项提供良好的抽象化处理。我的最爱是commander,受到同样名字的Ruby gem启发。


简单地:

$ npm install --save commander


将添加最新版本至我们的package.json。我们可以以简单的陈述方式定义我们的选项:

#!/usr/bin/env node
- console.log('Hello, world!');
+ var program = require('commander');
+
+ program
+  .arguments('<file>')
+  .option('-u, --username <username>', 'The user to authenticate as')
+  .option('-p, --password <password>', 'The user\'s password')
+  .action(function(file) {
+    console.log('user: %s pass: %s file: %s',
+        program.username, program.password, file);
+  })
+  .parse(process.argv);


这看起来相当容易。事实上,是一种保守说法。与Bash中的处理相比,这就是一件艺术品。至少,比起我写的那种Bash。


让我们对它进行一个快速测试。

$ snippet -u kannonboy -p correcthorsebatterystaple my_awesome_file
user: kannonboy pass: correcthorsebatterystaple file: my_awesome_file


酷毙了!基于以上我们提供的配置, commander还为我们产生了一些简单的帮助输出。

$ snippet --help

  Usage: snippet [options] <file>

  Options:

    -h, --help                 output usage information
    -u, --username <username>  The user to authenticate as
    -p, --password <password>  The user's password


因此我们就有了参数列表。但是,让用户输入明文密码有点让人难以接受。让我们来解决这个问题.


提示用户输入

让脚本获取信息的另一种方法是从标准输入读取process.stdin提供了这个功能不过,有些npm包提供了更好的API供我们使用。。这些中的大部分是基于回调或约定,但是我们打算使用co-prompt (基于co),因此我们能够利用ES6 yield关键字。这可以让我们书写无回调函数的async代码,看上去及感觉起来更...脚本化。

$ npm install --save co co-prompt


为了连同 co-prompt一起使用yield,我们需要用 co 包装一下我们的代码:

+ var co = require('co');
+ var prompt = require('co-prompt');
  var program = require('commander');
...
  .option('-u, --username <username>', 'The user to authenticate as')
  .option('-p, --password <password>', 'The user\'s password')
  .action(function(file) {
+    co(function *() {
+      var username = yield prompt('username: ');
+      var password = yield prompt.password('password: ');
       console.log('user: %s pass: %s file: %s',
-          program.username, program.password, file);
+          username, password, file);
+    });
  })
...


现在进行一个快速测试

$ snippet my_awesome_file
username: kannonboy
password: *************************
user: kannonboy pass: correcthorsebatterystaple file: my_awesome_file


好极了!唯一的窍门就是yield被引进了ES6中,因此如果用户正在运行node 4.0.0+, 这是唯一的开箱即用的工作。但是可以通过添加  --harmony 标记至我们的shebang使其向下兼容直至 0.11.2。

- #!/usr/bin/env node
+ #!/usr/bin/env node --harmony
  var co = require('co');
  var prompt = require('co-prompt');
...


STing片断

Bitbucket有一个很好用的代码片断管理 API。本例中我将集中于发布一个单一文件,但是如果您想做,我们能够发布完整的目录,更改访问权限,添加评论等。 我最喜欢的node HTTP客户端是superagent,因此让我们添加它至项目。

$ npm install --save superagent


现在让我们使用我们正在从用户收集的数据发布文件至服务器。superagent的一个优势是它有一个很好的附件处理 API

+ var request = require('superagent');
  var co = require('co');
  var prompt = require('co-prompt');
...
  .action(function(file) {
    co(function *() {
      var username = yield prompt('username: ');
      var password = yield prompt.password('password: ');
-     console.log('user: %s pass: %s file: %s',
-         file, username, password);
+     request
+       .post('https://api.bitbucket.org/2.0/snippets/')
+       .auth(username, password)
+       .attach('file', file)
+       .set('Accept', 'application/json')
+       .end(function (err, res) {
+         var link = res.body.links.html.href;
+         console.log('Snippet created: %s', link);
+       });
    });
  });
...


让我们试一下

$ snippet my_awesome_file
username: kannonboy
password: *************************
Snippet created: https://bitbucket.org/snippets/kannonboy/yq7r8


我们的片断是POSTed! \o/


处理错误情况

一般情况我们已经处理得很好了, 但是如果上传失败或者用户输入错误的证明文件又将怎样?UNIX处理它的方法是在标准错误 (standard error)输出一条信息,并使用非零代码退出,让我开始吧。

...
  request
    .post('https://api.bitbucket.org/2.0/snippets/')
    .auth(username, password)
    .attach('file', filename, file)
    .set('Accept', 'application/json')
    .end(function (err, res) {
+     if (!err && res.ok) {
        var link = res.body.links.html.href;
        console.log('Snippet created: %s', link);
+       process.exit(0);
+     }
+
+     var errorMessage;
+     if (res && res.status === 401) {
+       errorMessage = "Authentication failed! Bad username/password?";
+     } else if (err) {
+       errorMessage = err;
+     } else {
+       errorMessage = res.text;
+     }
+     console.error(errorMessage);
+     process.exit(1);
    });


这样做应该能搞定。


染色终端输出

如果您的用户正在使用一个合宜的shell,同样有一些包您可用来染色您的终端输出。我喜欢chalk,因为它有一个清楚的,可链接的API并且能够自动发现用户的shell是否支持染色。如果您想与Windows用户分享您的脚本,这会非常方便。

$ npm install --save chalk


chalk命令输出可以裹挟彩色及时尚字符串,并且很容易地串接常规字符串。

+ var chalk = require('chalk');
  var request = require('superagent');
  var co = require('co');
...
   .set('Accept', 'application/json')
   .end(function (err, res) {
     if (!err && res.ok) {
       var link = res.body.links.html.href;
-      console.log('Snippet created: %s', link);
+      console.log(chalk.bold.cyan('Snippet created: ') + link);
       process.exit(0);
     }

     var errorMessage;
     if (res && res.status === 401) {
       errorMessage = "Authentication failed! Bad username/password?";
     } else if (err) {
       errorMessage = err;
     } else {
       errorMessage = res.text;
     }
-    console.error(errorMessage);
+    console.error(chalk.red(errorMessage));
     process.exit(1);
  });


让我们试一试(这一次截图,您可以看到难以置信的色彩)。



因此现在我们有相当灵巧的小工具来创建文本片断。但是,如果是图像,PDF及其他大的二进制文件又将怎样?


添加一进度条


片断API事实上支持所有类型的文件,(直至10mb),但是大的文件或慢的网络连接会导致文件上传时命令看上去像死机。这种情况的命令行解决方法是是一个流行的 ASCII进度条。


关于渲染进度指示,progress是目前最流行的npm包。

$ npm install --save progress

progress API非常简单并具有相当的灵活性,唯一的问题是superagent的node版本没有一个我们可以订阅的事件来追踪我们的上传进行的怎样。


我们可以通过为我们的文件附件创建一个readablestream并添加一随着数据从磁盘流向请求而被触发的监听器将其解决。然后我们可以使用文件总大小初始化进度条,并且每当监听器被触发都将增加其进度。

 

+ var fs = require('fs');
+ var ProgressBar = require('progress');
  var chalk = require('chalk');
  var request = require('superagent');
...
  var username = yield prompt('username: ');
  var password = yield prompt.password('password: ');

+ var fileSize = fs.statSync(file).size;
+ var fileStream = fs.createReadStream(file);
+ var barOpts = {
+   width: 20,
+   total: fileSize,
+   clear: true
+ };
+ var bar = new ProgressBar(' uploading [:bar] :percent :etas', barOpts);
+
+ fileStream.on('data', function (chunk) {
+   bar.tick(chunk.length);
+ });

  request
    .post('https://api.bitbucket.org/2.0/snippets/')
    .auth(username, password)
-   .attach('file', file)
+   .attach('file', fileStream)
    .set('Accept', 'application/json')
...

现在是在一快速网络连接上进行的一~6mb 大小的文件。



好极了! 在他们等待上传完成同时用户现在可以看到某些内容。


总结

node中的命令行工具的可能性方面我们仅仅是谈了一些皮毛的东西。根据Atwood's Law,有npm包可用来处理标准输入,管理平行任务,监视文件,统配展开,压缩, ssh, git以及您使用Bash做的任何其他东西。此外,如果您需要退回另一个您无法找到一个合适的Java脚本实现的shell脚本或命令时,还有很好的API用来创建子进程。


以上我们为举例创建的源代码已经充分批准,并且在 Bitbucket上可用, 当然,已发布到npm。还有一些特性我没有在这里说明,比如OAuth,因此您不必每次都输入用户名和口令。您可以自己开始简单地使用它:

$ npm install -g bitbucket-snippet
$ snippet --help

原文链接:https://developer.atlassian.com/blog/2015/11/scripting-with-node/


CSDN开发服务为企业提供ALM(应用全生命周期管理)解决方案,致力于打造基于研发管理前沿、开放的工具产品集群(如Atlassian、Sonar、Jenkins等),结合CSDN CODE等研发工具的高效率、高质量和高可靠性企业级研发管理平台,为企业软件开发生命周期内各阶段、各部门、各角色提供全流程、全方位的跟踪和综合管理。截止目前,CSDN ALM解决方案已服务于包括华为、中国移动通信研究院、嘀嘀打车、广联达、招商银行、南粤银行等在内的数百家行业企业及互联网企业。

0
0

猜你在找
查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:552491次
    • 积分:3137
    • 等级:
    • 排名:第10380名
    • 原创:21篇
    • 转载:35篇
    • 译文:9篇
    • 评论:55条
    最新评论