Skip to content

Latest commit

 

History

History
764 lines (536 loc) · 38.5 KB

File metadata and controls

764 lines (536 loc) · 38.5 KB

五、命令行界面

前四章向您展示了 Node 开发的基础。从这一章开始,这本书改变了方向,开始关注用于创建 Node 应用的各种 API 和模块。本章重点介绍如何创建命令行界面(CLI)来与用户进行交互。首先,您将学习 Node 内置 API 的命令行基础。从那里,你可以使用commander模块扩展基础,你可能记得在第二章中的几个npm例子。

命令行参数

命令行参数是向计算机程序提供输入的最基本的方式之一。在 Node 应用中,命令行参数可以通过全局process对象的argv数组属性 来访问。清单 5-1 展示了如何使用forEach()方法迭代argv,就像任何其他数组一样。

清单 5-1 。一个迭代argv数组的例子

process.argv.forEach(function(arg, index) {
  console.log("argv[" + index + "] = " + arg);
});

为了检查保存在argv中的实际值,将来自清单 5-1 的代码保存在一个名为argv-test.js 的新 JavaScript 源文件中。接下来,运行代码,观察输出(参见清单 5-2 )。注意,有四个参数被传递给了我们的 Node 程序:-foo3--bar=4-baz。然而,基于程序的输出,在argv中有六个元素。无论您提供什么样的命令行参数组合,argv总是在数组的开头包含额外的两个元素。这是因为argv的前两个元素总是node(可执行文件的名称)和 JavaScript 源文件的路径。argv数组的其余部分由实际的命令行参数组成。

清单 5-2 。运行清单 5-1 中代码的输出

$ node argv-test.js -foo 3 --bar=4 -baz
argv[0] = node
argv[1] = /home/colin/argv-test.js
argv[2] = -foo
argv[3] = 3
argv[4] = --bar=4
argv[5] = -baz

解析参数值

基于清单 5-2 中的命令行,我们似乎试图传入三个参数:foobarbaz。然而,这三个论点的作用各不相同。foo的值来自它后面的自变量(我们假设它是一个整数)。在这种情况下,foo的值是3。与foo不同的是,bar4的值被编码在同一个参数中,后面跟一个等号。同时,baz是一个布尔自变量。如果提供了参数,则其值为true,否则为false。不幸的是,通过简单地检查argv中的值,这些语义都没有被捕获。

为了提取正确的命令行参数值,我们可以开发一个定制的解析器(见清单 5-3 )。在示例中,parseArgs()函数 负责解析命令行、提取值并返回一个对象,该对象将每个参数映射到其正确的值。这个函数的工作方式是循环遍历argv中的每个元素,检查可识别的参数名。如果参数是foo,那么从下面的参数中解析出一个整数。循环变量i也被递增以节省时间,因为没有必要为foo的值执行循环体。如果自变量被确定为baz,我们简单的赋值true。为了提取bar的值,使用了一个正则表达式。如果字符串--bar=后跟一系列一个或多个数字,那么这些数字将被解析为一个整数值。最后,所有的参数都通过args对象返回,并打印到控制台。

清单 5-3 。清单 5-2 中示例的命令行解析器

function parseArgs() {
  var argv = process.argv;
  var args = {
    baz: false
  };

  for (var i = 0, len = argv.length; i < len; i++) {
    var arg = argv[i];
    var match;

    if (arg === "-foo") {
      args.foo = parseInt(argv[++i]);
    } else if (arg === "-baz") {
      args.baz = true;
    } else if (match = arg.match(/--bar=(\d+)/)) {
      args.bar = parseInt(match[1]);
    }
  }

  return args;
}

var args = parseArgs();

console.log(args);

清单 5-4 显示了运行清单 5-3 中代码的输出。如你所见,所有的论点都被恰当地提取出来了。但是当用户输入格式错误时会发生什么呢?清单 5-5 显示了使用不同参数运行相同程序的输出。在这种情况下,baz被拼错为az,用户忘记为foo提供一个值。

清单 5-4 。运行清单 5-3 中代码的结果

$ node argv-parser.js -foo 3 --bar=4 -baz
{ foo: 3, bar: 4, baz: true }

清单 5-5 。由畸形的用户输入产生的输出

$ node argv-parser.js -foo -az --bar=4
{ foo: NaN, bar: 4 }

在清单 5-5 的输出中,请注意baz完全缺失,而foo的值为NaN(非数字),因为解析器试图将-az转换为整数。由于baz没有从命令行传入,理想情况下它的值是false。类似地,foobar应该有一些默认值,以便处理这样的情况。在这种情况下,预填充parseArgs()中的args对象不会阻止foo被设置为NaN

相反,我们可以使用一个sanitize()函数对args进行后处理(参见清单 5-6 )。这个函数检查每个参数的值,如果它还没有值,就给它分配一个合适的值。在这个例子中,JavaScript 内置的isFinite()方法用于确保foobar是有效的整数。由于baz是一个布尔值,代码简单地检查它是否不等于true,如果是,就将其设置为false。这确保了baz实际上被设置为布尔值false——而不是保留为undefined,这是一个不同的 falsy 值。注意parseArgs()代码不包括在本例中,因为它没有改变。

清单 5-6 。为参数分配默认值的sanitize()函数

function sanitize(args) {
  if (!isFinite(args.foo)) {
    args.foo = 0;
  }

  if (!isFinite(args.bar)) {
    args.bar = 0;
  }

  if (args.baz !== true) {
    args.baz = false;
  }

  return args;
}

var args = sanitize(parseArgs());

console.log(args);

commander 中的命令行参数

如果实现简单的命令行解析所需的工作量对您来说似乎有点多,请放心,您并不孤单。幸运的是,像commander这样的模块使得命令行解析变得简单。第三方模块commander用于简化常见的 CLI 任务,如参数解析和读取用户输入。要安装commander,使用命令npm install commander。为了适应命令行参数解析,commander 提供了option()parse()方法。对option()的每次调用都向commander注册一个有效的命令行参数。一旦使用option()注册了所有可能的参数,就可以使用parse()方法从命令行提取参数值。

用一个例子来说明commander的命令行参数系统是如何工作的可能是最简单的。在清单 5-7 中,commander被配置为接受三个参数:--foo--bar--baz。也可以使用-f来指定--foo参数。这被认为是论点的简短版本。所有的commander参数必须有一个短名称和一个长名称。短名称应该是一个破折号后跟一个字母,长名称应该在名称前有两个破折号。

清单 5-7 。使用commander的命令行解析器示例

var commander = require("commander");

commander
  .option("-f, --foo <i>", "Integer value for foo", parseInt, 0)
  .option("-b, --bar [j]", "Integer value for bar", parseInt, 0)
  .option("-z, --baz", "Boolean argument baz")
  .parse(process.argv);

console.log(commander.foo);
console.log(commander.bar);
console.log(commander.baz);

注意--foo--bar后面的<i>[j]。这些是应该跟在参数后面的值。当使用尖括号时,就像使用--foo一样,必须指定附加值,否则会抛出一个错误。与--bar一起使用的方括号表示附加值是可选的。--baz被视为布尔参数,因为它不接受任何附加参数。参数字符串之后是描述字符串。这些字符串是人类可读的,用于显示帮助,这将被暂时覆盖。

接下来要指出的是,--foo--bar选项也指parseInt()和数字 0(零)。parseInt()作为可选参数传递,用于解析附加参数。在这种情况下,--foo--bar的值被评估为整数。最后,如果没有为--foo--bar提供值,它们将被设置为 0。

一旦注册了所有选项,就调用parse()来处理命令行。从技术上讲,任何数组都可以传递给parse(),但是传入process.argv最有意义。解析后,参数值根据它们的长名称可用,如三个 print 语句所示。

自动生成的帮助

commander根据选项配置自动生成一个--help(或-h)自变量。清单 5-8 显示了前一个例子中自动生成的帮助。

清单 5-8 。为清单 5-7 中的代码自动生成帮助

$ node commander-test.js --help

  Usage: commander-test.js [options]

  Options:

    -h, --help         output usage information
    -f, --foo <i>      Integer value for foo
    -b, --bar [j]      Integer value for bar
    -z, --baz          Boolean argument baz

还有两种方法可以用来显示帮助:help()outputHelp()。它们之间唯一的区别是help()会导致程序退出,而outputHelp()不会。通常,如果提供了无效的参数,您可以调用help(),然后退出。但是,如果你想显示帮助菜单并出于某种原因继续执行,你可以调用outputHelp()。这两种方法的使用如清单 5-9 所示。

清单 5-9 。使用commander帮助方法

commander.help()
commander.outputHelp()

标准流

默认情况下,Node 应用连接到提供输入和输出功能的三个数据流— stdinstdoutstderr。如果您熟悉 C/C++、Java 或任何一种其他语言,您肯定以前遇到过这些标准流。本节将详细探讨每一个问题。

标准输入

stdin流(标准输入的缩写)是一个可读的流,为程序提供输入。默认情况下,stdin从用于启动应用的终端窗口接收数据,and通常用于在运行时接受用户的输入。然而,stdin也可以从一个文件或另一个程序接收它的数据。

在 Node 应用中,stdin是全局process对象的属性。但是,当应用启动时,stdin处于暂停状态,也就是说,不能从中读取任何数据。对于要读取的数据,必须使用resume()方法对数据流进行解析(见清单 5-10 ),该方法不带参数,也不提供返回值。

清单 5-10stdin.resume() 的用法

process.stdin.resume()

除了解除对stdin流的暂停,resume()还防止应用终止,因为它将处于等待输入的状态。然而,stdin可以再次暂停,使用pause()方法,允许程序退出。清单 5-11 显示了pause()的用法。

清单 5-11stdin.pause() 的用法

process.stdin.pause()

调用resume()后,你的程序可以从stdin读取数据。但是,您需要设置一个data事件处理程序来自己读取数据。stdin上新数据的到达触发了一个data事件。data事件处理程序接受一个参数,即接收到的数据。在清单 5-12 中,显示了如何使用data事件从stdin读取数据,提示用户输入他/她的名字。然后调用resume()以激活stdin流。一旦输入了名字,用户按下Return,就会调用data事件处理程序——使用once()方法添加的(在第 4 章中介绍)。然后事件处理器确认用户并暂停stdin。注意,在事件处理程序中,data参数被转换成一个字符串。这样做是因为data是作为Buffer对象传入的。用于处理 Node 应用中的原始二进制数据。(该主题在第 8 章的中有更详细的介绍。)

清单 5-12 。从stdin读取数据的示例

process.stdin.once("data", function(data) {
  var response = data.toString();

  console.log("You said your name is " + response);
  process.stdin.pause();
});

console.log("What is your name?");
process.stdin.resume();

通过预先指定stdin流的字符编码,可以避免每次读取数据时都必须将数据转换成字符串。为此,请使用stdinsetEncoding()方法。如表 5-1 所示,Node 支持许多不同的字符编码。处理字符串数据时,建议将编码设置为utf8 (UTF-8)。清单 5-13 展示了如何使用setEncoding()重写清单 5-12

表 5-1 。Node 支持的各种字符串编码类型

|

编码类型

|

描述

| | --- | --- | | utf8 | 多字节编码的 Unicode 字符。UTF-8 编码被许多网页使用,并用于表示 Node 中的字符串数据。 | | ascii | 七位美国信息交换标准码(ASCII)编码。 | | utf16le | 小端编码的 Unicode 字符。每个字符是两个或四个字节。 | | ucs2 | 这只是utf16le编码的别名。 | | base64 | Base64 字符串编码。Base64 通常用于 URL 编码、电子邮件和类似的应用。 | | binary | 允许仅使用每个字符的前八位将二进制数据编码为字符串。这种编码现在已被弃用,取而代之的是Buffer对象,并将在 Node 的未来版本中删除。 | | hex | 将每个字节编码为两个十六进制字符。 |

清单 5-13 。设置字符编码类型后从stdin读取

process.stdin.once("data", function(data) {
  console.log("You said your name is " + data);
  process.stdin.pause();
});

console.log("What is your name?");
process.stdin.setEncoding("utf8");
process.stdin.resume();

使用commanderstdin读取

commander模块还提供了几种从stdin读取数据的有用方法。其中最基本的是prompt(),它向用户显示一些消息或问题,然后读入响应。然后将响应作为字符串传递给回调函数进行处理。清单 5-14 展示了如何使用prompt()重写来自清单 5-13 的例子。

清单 5-14 。使用commanderprompt()方法从stdin读取

var commander = require("commander");

commander.prompt("What is your name? ", function(name) {
  console.log("You said your name is " + name);
  process.stdin.pause();
});

confirm()

confirm()方法与prompt()相似,但用于解析布尔响应。如果用户输入yyestrue或 ok,回调将被调用,其参数设置为true。否则,回调将被调用,其参数设置为false。清单 5-15 中显示了confirm()方法的一个使用示例,清单 5-16 显示了该示例的示例输出。

清单 5-15 。使用commanderconfirm()方法解析布尔响应

var commander = require("commander");

commander.confirm("Continue? ", function(proceed) {
  console.log("Your response was " + proceed);
  process.stdin.pause();
});

清单 5-16 。运行清单 5-15 中代码的输出示例

$ node confirm-example.js
Continue? yes
Your response was true

password()

prompt()的另一个特例是password()方法,它用于获取敏感的用户输入,而不在终端窗口中显示。顾名思义,它最大的用例是提示用户输入密码。清单 5-17 中的显示了一个使用password()的例子。

清单 5-17 。使用password()方法提示输入密码

var commander = require("commander");

commander.password("Password: ", function(password) {
  console.log("I know your password!  It's " + password);
  process.stdin.pause();
});

默认情况下,password()不会将信息回显到终端。但是,可以提供一个可选的掩码字符串,它会为用户输入的每个字符回显。清单 5-18 显示了一个例子。其中,掩码字符串只是星号字符(*)。

清单 5-18 。使用掩码字符提示输入密码

var commander = require("commander");

commander.password("Password: ", "*", function(password) {
  console.log("I know your password!  It's " + password);
  process.stdin.pause();
});

choose()

choose()功能对于创建基于文本的菜单很有用。以一组选项作为第一个参数,choose()允许用户从列表中选择一个选项。第二个参数是用所选选项的数组索引调用的回调。清单 5-19 显示了一个使用choose()的例子。

清单 5-19 。使用choose()显示文本菜单

var commander = require("commander");
var list = ["foo", "bar", "baz"];

commander.choose(list, function(index) {
  console.log("You selected " + list[index]);
  process.stdin.pause();
});

清单 5-20 显示了运行前一个例子的样本输出。需要注意的一点是,菜单项计数从 1 开始,而数组从 0 开始索引。考虑到这一点,choose()将正确的从零开始的数组索引传递给回调函数。

清单 5-20 。清单 5-19 的输出示例

$ node choose-example.js
  1) foo
  2) bar
  3) baz
  : 2
You selected bar

标准输出

标准输出,或stdout ,是一个可写的流,程序应该将它们的输出指向这个流。默认情况下,Node 应用直接输出到启动应用的终端窗口。向stdout写入数据的最直接方式是通过process.stdout.write()方法。write()的用法如清单 5-21 所示。write()的第一个参数是要写入的数据字符串。第二个参数是可选的;用于指定数据的字符编码,默认为utf8 (UTF-8)编码。write()支持表 5-1 中指定的所有编码类型。write()的最后一个参数是可选的回调函数。一旦数据成功写入stdout,就会执行该命令。没有参数传递给回调函数。

清单 5-21stdout的使用。write()方法

process.stdout.write(data, [encoding], [callback])

image process.stdout.write()也可以接受一个Buffer作为它的第一个自变量。

console.log()

阅读完stdout.write()之后,你可能会好奇它与已经讨论过的console.log()方法有什么关系。实际上,console.log()只是一个在引擎盖下调用stdout.write()的包装器。清单 5-22 显示了console.log()的源代码。这段代码直接取自 Node 官方 GitHub repo 中的文件https://github.com/joyent/node/blob/master/lib/console.js。如您所见,log()调用了_stdout.write()。检查整个源文件会发现_stdout只是对stdout的引用。

清单 5-22console.log()的源代码

Console.prototype.log = function() { this._stdout.write(util.format.apply(this, arguments) + '\n');
};

另外,注意对write()的调用调用了util.format()方法。util对象是对核心util模块的引用。format()方法用于根据传递给它的参数创建格式化字符串。作为第一个参数,format()接受一个包含零个或多个占位符的格式字符串。占位符是格式字符串中的一个字符序列,预计将被返回的字符串中的不同值替换。在格式字符串之后,format()期望每个占位符都有一个额外的参数。format()支持四种占位符,如表 5-2 所述。

表 5-2 。util.format()支持的各种占位符。

|

占位符

|

更换

| | --- | --- | | %s | 字符串数据。一个参数被使用并传递给String()构造函数。 | | %d | 整数或浮点数字数据。一个参数被使用并传递给Number()构造函数。 | | %j | JSON 数据。一个参数被消费并传递给JSON.stringify()。 | | %% | 一个百分号(%)字符。这不会消耗任何参数。 |

清单 5-23 中显示了util.format()的几个例子,清单 5-24 中显示了的结果输出。这些示例显示了如何使用各种占位符替换数据。前三个示例使用字符串、数字和 JSON 占位符来替换字符串。请注意,数字占位符被替换为NaN。这是因为保存在name变量中的字符串不能被转换成实际数字。在第四个例子中,使用了 JSON 占位符,但是没有相应的参数传递给format()。结果就是没有替换发生,并且%j包含在结果中。在第五个例子中,format()比它能处理的多传递了一个参数。format()通过将附加参数转换为字符串并附加到结果字符串中来处理附加参数,使用空格字符作为分隔符。在第六个示例中,按照预期使用了多个占位符。最后,在第七个示例中,根本没有提供任何格式字符串。在这种情况下,参数被转换为字符串,并用空格字符分隔符连接起来。

清单 5-23 。使用util.format()的几个例子

var util = require("util");
var name = "Colin";
var age = 100;
var format1 = util.format("Hi, my name is %s", name);
var format2 = util.format("Hi, my name is %d", name);
var format3 = util.format("Hi, my name is %j", name);
var format4 = util.format("Hi, my name is %j");
var format5 = util.format("Hi, my name is %j", name, name);
var format6 = util.format("I'm %s, and I'm %d years old", name, age);
var format7 = util.format(name, age);

console.log(format1);
console.log(format2);
console.log(format3);
console.log(format4);
console.log(format5);
console.log(format6);
console.log(format7);

清单 5-24 。运行清单 5-23 中的代码的输出

$ node format.js
Hi, my name is Colin
Hi, my name is NaN
Hi, my name is "Colin"
Hi, my name is %j
Hi, my name is "Colin" Colin
I'm Colin, and I'm 100 years old
Colin 100

image 注意任何熟悉 C/C++、PHP 或其他语言的人都会认识到util.format()的行为,因为它提供了类似于printf()函数的格式。

其他打印功能

Node 还提供了几个不太流行的函数来打印到stdout。例如,util模块定义了log()方法。log()方法接受一个单独的字符串作为参数,并把它和时间戳一起打印给stdout清单 5-25 显示了log()的一个实例。结果输出如清单 5-26 所示。

清单 5-25util.log()的一个例子

var util = require("util");

util.log("baz");

清单 5-26 。运行清单 5-25 中的代码的输出

$ node util-log-method.js
17 Mar 15:08:29 - baz

console对象还提供了两种额外的打印方法,info()dir()info()方法只是console.log()的别名。console.dir()将一个对象作为其唯一参数。使用util.inspect()方法将对象字符串化,然后打印到stdoututil.inspect()是用于将多余的参数字符串化到没有相应占位符的util.format()的相同方法。inspect(),一个强大的字符串化数据的方法,将在下面介绍。

util.inspect()

util.inspect()用于将对象转换成格式良好的字符串。虽然它真正的强大之处在于它的定制能力,但我们首先来看看它的默认行为。清单 5-27 显示了一个使用inspect()字符串化一个对象obj的例子。结果字符串如清单 5-28 中的所示。

清单 5-27 。一个使用util.inspect()方法的例子

var util = require("util");
var obj = {
  foo: {
    bar: {
      baz: {
        baff: false,
        beff: "string value",
        biff: null
      },
      boff: []
    }
  }
};

console.log(util.inspect(obj));

清单 5-28 。清单 5-27 中的util.inspect()创建的字符串

{ foo: { bar: { baz: [Object], boff: [] } } }

注意foobar是完全字符串化的,但是baz只显示字符串[Object]。这是因为,默认情况下,inspect()在格式化对象时只通过两级递归。不过,这种行为可以通过使用可选的第二个参数inspect()来改变。该参数是一个指定inspect()配置选项的对象。如果你对增加递归的深度感兴趣,设置depth选项。它可以设置为null来强制inspect()在整个对象上递归。清单 5-29 和清单 5-30 中显示了这样的例子和结果字符串。

清单 5-29 。在启用完全递归的情况下调用util.inspect()

var util = require("util");
var obj = {
  foo: {
    bar: {
      baz: {
        baff: false,
        beff: "string value",
        biff: null
      },
      boff: []
    }
  }
};

console.log(util.inspect(obj, {
  depth: null
}));

清单 5-30 。运行清单 5-29 中的代码的输出

$ node inspect-recursion.js
{ foo:
   { bar:
      { baz: { baff: false, beff: 'string value', biff: null },
        boff: [] } } }

options参数支持其他几个选项— showHiddencolorscustomInspectshowHiddencolors默认为false,而customInspect默认为true。当showHidden设置为true时,inspect()打印对象的所有属性,包括不可枚举的属性。将colors设置为true会导致结果字符串采用 ANSI 颜色代码。当customInspect设置为true时,对象可以定义自己的inspect()方法,调用这些方法可以返回字符串化过程中使用的字符串。在这个例子中,如清单 5-31 所示,一个自定义的inspect()方法被添加到顶层对象中。此自定义方法返回隐藏所有子对象的字符串。结果输出如清单 5-32 所示。

image 注意并不是所有的方法属性都是相同的。在 JavaScript 中,可以创建不可枚举的属性,当一个对象在for...in循环中迭代时,这些属性不会显示出来。通过设置showHidden选项,inspect()将在其输出中包含不可枚举的属性。

清单 5-31 。使用自定义的inspect()方法调用util.inspect()

var util = require("util");
var obj = {
  foo: {
    bar: {
      baz: {
        baff: false,
        beff: "string value",
        biff: null
      },
      boff: []
    }
  },
  inspect: function() {
    return "{Where'd everything go?}";
  }
};

console.log(util.inspect(obj));

清单 5-32 。清单 5-31 中自定义inspect()方法的结果

$ node inspect-custom.js
{Where'd everything go?}

标准误差

标准误差流stderr是类似于stdout的输出流。然而,stderr用于显示错误和警告信息。虽然stderrstdout是相似的,stderr是一个独立的实体,所以你不能像console.log()一样使用stdout函数来访问它。幸运的是,Node 提供了许多专门用于访问stderr的函数。对stderr最直接的访问路径是通过它的write()方法。write()的用法如清单 5-33 所示,与stdoutwrite()方法相同。

清单 5-33 。使用stderr write()方法

process.stderr.write(data, [encoding], [callback])

console对象还提供了两个方法error()warn(),用于写入stderrconsole.warn()的行为与console.log()完全一样,只是充当了process.stderr.write()的包装器。error()方法只是warn()的别名。清单 5-34 显示了warn()error()的源代码。

清单 5-34console.warn()console.error() 的源代码

Console.prototype.warn = function() {
  this._stderr.write(util.format.apply(this, arguments) + '\n');
};

Console.prototype.error = Console.prototype.warn;

console.trace()

console对象还提供了一个有用的调试方法,名为trace(),它创建并打印一个堆栈跟踪到stderr,而不会使程序崩溃。如果您曾经遇到过错误(我相信您现在已经遇到过了),那么您就会看到程序崩溃时打印的堆栈跟踪。trace()完成同样的事情,没有错误和崩溃。清单 5-35 显示了一个使用trace()的例子,其输出显示在清单 5-36 中。在示例中,名为test-trace的堆栈跟踪是在函数baz()中创建的,该函数从bar()中调用,而后者又从foo()中调用。请注意,这些函数是堆栈跟踪中的前三项。堆栈跟踪中的其余函数是由 Node 框架进行的调用。

清单 5-35 。使用console.trace() 生成示例堆栈跟踪

(function foo() {
  (function bar() {
    (function baz() {
      console.trace("test-trace");
    })();
  })();
})();

清单 5-36 。运行清单 5-35 中的示例的输出

$ node stack-trace.js
Trace: test-trace
    at baz (/home/colin/stack-trace.js:4:15)
    at bar (/home/colin/stack-trace.js:5:7)
    at foo (/home/colin/stack-trace.js:6:5)
    at Object.<anonymous> (/home/colin/stack-trace.js:7:3)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)

image 传递给console.trace()的自变量被转发给util.format()。因此,可以使用格式字符串创建堆栈跟踪名称。

分离stderrstdout

stderr定向到与stdout相同的目的地是常见的,但不是必需的。默认情况下,Node 的stdoutstderr都指向运行流程的终端窗口。但是,可以重定向一个流或两个流。清单 5-37 中的代码可以用来简单地演示这个概念。示例代码使用console.log()stdout输出一条消息,使用console.error()stderr输出第二条消息。

清单 5-37 。打印到stdoutstderr的示例应用

console.log("foo");
console.error("bar");

清单 5-37 中的代码正常运行时,两条消息都被打印到终端窗口。输出如清单 5-38 所示。

清单 5-38 。运行清单 5-37 中的代码时的控制台输出

$ node stdout-and-stderr.js
foo
bar

同样的代码在清单 5-39 中再次执行。然而,这次使用>操作符将stdout重定向到文件output.txt。注意重定向对stderr流没有影响。结果是发送到stderrbar打印在终端窗口,而foo没有。

清单 5-39 。当stdout被重定向时清单 5-39 中代码的控制台输出

$ node stdout-and-stderr.js > output.txt
bar

image 注意您可能已经注意到了,console方法是同步的。这种行为(当底层流的目的地是文件或终端窗口时的默认行为)避免了由于程序崩溃或退出而丢失消息。在第 7 章中有更多关于流和它们如何被管道化的内容,但是现在,只需要知道当底层流被管道化时console方法的行为是异步的。

TTY 界面

正如您已经看到的,默认情况下,标准流被配置为使用终端窗口。为了适应这种配置,Node 提供了一个 API 来检查终端窗口的状态。因为流可以被重定向,所以所有标准流都提供了一个isTTY属性,如果流与终端窗口相关联,那么这个属性就是true清单 5-40 显示了如何为每个流访问这些属性。默认情况下,isTTYstdinstdoutstderrtrue,如清单 5-41 所示。

清单 5-40 。检查每个标准流是否连接到终端的示例

console.warn("stdin  = " + process.stdin.isTTY);
console.warn("stdout = " + process.stdout.isTTY);
console.warn("stderr = " + process.stderr.isTTY);

清单 5-41 。默认条件下清单 5-40 的输出

$ node is-tty.js
stdin  = true
stdout = true
stderr = true

清单 5-42 展示了当stdout被重定向到一个文件时,这些值是如何变化的。注意源代码使用了console.warn()而不是console.log()。这是有意这样做的,以便stdout可以被重定向,同时仍然提供控制台输出。如你所料,isTTY的值不再是stdouttrue。然而,请注意isTTY不是false,而是简单的undefined,这意味着isTTY不是所有流的属性,只是那些与终端相关的流的属性。

清单 5-42 。来自清单 5-40 的输出,带有重定向的stdout

$ node is-tty.js > output.txt
stdin  = true
stdout = undefined
stderr = true

确定终端尺寸

终端窗口的大小,尤其是列数,会极大地影响程序输出的可读性。因此,一些应用可能需要根据终端大小定制输出。假设stdoutstderr或两者都与终端窗口相关联,则可以确定终端中的行数和列数。这些信息可以分别通过流的rowscolumns属性获得。您还可以使用流的getWindowSize()方法以数组的形式检索终端维度。列表 5-43 显示了如何确定端子尺寸,而列表 5-44 显示了最终输出。

清单 5-43 。以编程方式确定终端窗口的大小

var columns = process.stdout.columns;
var rows = process.stdout.rows;

console.log("Size:  " + columns + "x" + rows);

清单 5-44 。运行清单 5-43 中的代码的输出

$ node tty-size.js
Size:  80x24

image 注意使用stdin无法确定终端大小,因为终端尺寸仅与可写 TTY 流相关。

如果你的程序的输出依赖于终端的大小,那么当用户在运行时调整窗口大小时会发生什么?幸运的是,可写 TTY 流提供了一个resize事件,该事件在终端窗口调整大小时触发。清单 5-45 中的例子定义了一个函数size(),它打印出当前的端子尺寸。启动时,程序首先检查stdout是否连接到终端窗口。如果不是,将显示一条错误消息,并且程序通过调用process.exit()方法以一个错误代码终止。如果程序在终端窗口中运行,它会通过调用size()来显示窗口的当前大小。相同的函数随后被用作resize事件处理程序。最后,调用process.stdin.resume()来防止程序在测试时终止。

清单 5-45 。监控终端大小的示例

function size() {
  var columns = process.stdout.columns;
  var rows = process.stdout.rows;

  console.log("Size:  " + columns + "x" + rows);
}

if (!process.stdout.isTTY) {
  console.error("Not using a terminal window!");
  process.exit(-1);
}

size();
process.stdout.on("resize", size);
process.stdin.resume();

信号事件

信号是发送给特定进程或线程的异步事件通知。它们用于在符合 POSIX 的操作系统上提供有限形式的进程间通信。(如果您正在为 Windows 开发,您可能希望跳过这一部分。)所有信号及其含义的完整列表超出了本书的范围,但这些信息在互联网上很容易找到。

例如,如果您在终端程序运行时按下Ctrl+C,一个中断信号SIGINT将被发送到该程序。在 Node 应用中,除非提供了自定义处理程序,否则信号由默认处理程序处理。当默认处理程序接收到一个SIGINT信号时,它会导致程序终止。要覆盖这种行为,向process对象添加一个SIGINT事件处理程序,如清单 5-46 中的所示。

清单 5-46 。添加一个SIGINT信号事件处理器

process.on("SIGINT", function() {
  console.log("Got a SIGINT signal");
});

image 注意如果你在你的应用中包含了来自清单 5-46 的事件处理程序,你将无法使用Ctrl+C终止程序。但是,您仍然可以使用Ctrl+D停止程序。

用户环境变量

环境变量是操作系统级别的变量,可由系统上执行的进程访问。例如,许多操作系统定义了一个TEMPTMP环境变量,它指定了用于保存临时文件的目录。在 Node 中访问环境变量非常简单。process对象有一个包含用户环境的对象属性envenv对象可以像任何其他对象一样进行交互。清单 5-47 显示了如何引用env对象。在本例中,显示了PATH变量。然后在PATH的开头添加一个额外的 Unix 风格的目录。最后显示刚更新的PATH清单 5-48 显示了这个例子的输出。但是,请注意,根据您当前的系统配置,您自己的输出可能会有很大的不同。

清单 5-47 。使用用户环境变量的示例

console.log("Original: " + process.env.PATH);
process.env.PATH = "/some/path:" + process.env.PATH;
console.log("Updated:   " + process.env.PATH);

清单 5-48 。运行清单 5-47 中代码的输出示例

$ node env-example.js
Original:  /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
Updated:   /some/path:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin

环境变量通常用于配置应用中不同的执行模式。例如,一个程序可能支持两种执行模式,开发和生产。在开发模式下,调试信息可能会打印到控制台,而在生产模式下,调试信息可能会记录到文件中或被完全禁用。要启用开发模式,只需设置一个环境变量,该变量可以从应用内部访问。清单 5-49 展示了这个概念是如何工作的。在这个例子中,DEVELOPMENT环境变量被用来定义布尔变量devMode,然后控制if语句的条件。注意,!! (bang bang)符号用于强制将任何值转换为布尔值。

清单 5-49 。使用环境变量实现开发模式的一个例子

var devMode = !!process.env.DEVELOPMENT;

if (devMode) {
  console.log("Some useful debugging information");
}

清单 5-50 显示了在开发模式下执行前面例子的一种方法。请注意,如何在启动 Node 的同一个命令提示符下定义环境变量,从而实现快速的一次性测试,避免了实际定义环境变量的麻烦。(不过,那也可以。)

清单 5-50 。在开发模式下运行清单 5-51 中的例子

$ DEVELOPMENT=1 node dev-mode.js
Some useful debugging information

摘要

本章介绍了 Node 中命令行界面编程的基础知识。一些例子甚至展示了来自 Node 核心的实际代码。现在,您应该已经掌握了命令行参数、标准流、信号处理程序和环境变量等基本概念。这些概念集合了一些已经介绍过的内容(比如事件处理程序)和一些将在本书后面介绍的内容(比如流)。

本章还向您展示了commander模块的基础知识。在撰写本文时,commandernpm注册表中第六大依赖模块。但是,您可能有兴趣探索其他类似的 CLI 模块。其中最突出的是optimist模块(optimist由 James Halliday——又名 substack——Node 社区的杰出成员创建)。我们鼓励您浏览npm存储库并尝试其他模块,以找到最适合您需求的模块。