util.parseArgs() 解析命令行参数parseArgs()parseArgs 令牌在本章中,我们将探讨如何使用模块 node:util 中的 Node.js 函数 parseArgs() 来解析命令行参数。
本章中的每个示例都隐含了以下两个导入
import * as assert from 'node:assert/strict';
import {parseArgs} from 'node:util';第一个导入用于我们用来检查值的测试断言。第二个导入用于本章主题函数 parseArgs()。
处理命令行参数涉及以下步骤
process.argv 中的数组接收这些单词。process 是 Node.js 上的全局变量。parseArgs() 将该数组转换为更易于使用的形式。让我们使用以下带有 Node.js 代码的 Shell 脚本 args.mjs 来查看 process.argv 的外观
#!/usr/bin/env node
console.log(process.argv);我们从一个简单的命令开始
% ./args.mjs one two
[ '/usr/bin/node', '/home/john/args.mjs', 'one', 'two' ]
如果我们在 Windows 上通过 npm 安装该命令,则相同的命令会在 Windows 命令 Shell 上产生以下结果
[
'C:\\Program Files\\nodejs\\node.exe',
'C:\\Users\\jane\\args.mjs',
'one',
'two'
]无论我们如何调用 Shell 脚本,process.argv 始终以用于运行我们代码的 Node.js 二进制文件的路径开头。接下来是我们脚本的路径。数组以传递给脚本的实际参数结尾。换句话说:脚本的参数始终从索引 2 开始。
因此,我们更改脚本,使其如下所示
#!/usr/bin/env node
console.log(process.argv.slice(2));让我们尝试更复杂的论点
% ./args.mjs --str abc --bool home.html main.js
[ '--str', 'abc', '--bool', 'home.html', 'main.js' ]
这些论点包括
--str,其值为文本 abc。这样的选项称为*字符串选项*。--bool 没有关联值 - 它是一个存在或不存在的标志。这样的选项称为*布尔选项*。home.html 和 main.js。两种使用参数的风格很常见
写成 JavaScript 函数调用,前面的示例如下所示(在 JavaScript 中,选项通常放在最后)
argsMjs('home.html', 'main.js', {str: 'abc', bool: false});如果我们想让 parseArgs() 解析带有参数的数组,我们首先需要告诉它我们的选项是如何工作的。假设我们的脚本有
--verbose--times 接收非负整数。parseArgs() 对数字没有特殊支持,因此我们必须将其设为字符串选项。--color我们将这些选项描述给 parseArgs(),如下所示
const options = {
'verbose': {
type: 'boolean',
short: 'v',
},
'color': {
type: 'string',
short: 'c',
},
'times': {
type: 'string',
short: 't',
},
};只要 options 的属性键是有效的 JavaScript 标识符,则是否对其进行引用取决于您。两者都有优缺点。在本章中,它们始终被引用。这样,具有非标识符名称的选项(例如 my-new-option)看起来与具有标识符名称的选项相同。
options 中的每个条目都可以具有以下属性(通过 TypeScript 类型定义)
type Options = {
type: 'boolean' | 'string', // required
short?: string, // optional
multiple?: boolean, // optional, default `false`
};.type 指定选项是布尔值还是字符串。.short 定义选项的简短版本。它必须是单个字符。我们很快就会看到如何使用简短版本。.multiple 指示一个选项最多可以使用一次还是可以使用零次或多次。我们稍后会看到这意味着什么。以下代码使用 parseArgs() 和 options 来解析带有参数的数组
assert.deepEqual(
parseArgs({options, args: [
'--verbose', '--color', 'green', '--times', '5'
]}),
{
values: {__proto__:null,
verbose: true,
color: 'green',
times: '5'
},
positionals: []
}
);存储在 .values 中的对象的原型为 null。这意味着我们可以使用 in 运算符来检查属性是否存在,而不必担心继承的属性(例如 .toString)。
如前所述,作为 --times 值的数字 5 被处理为字符串。
我们传递给 parseArgs() 的对象的 TypeScript 类型如下
type ParseArgsProps = {
options?: {[key: string], Options}, // optional, default: {}
args?: Array<string>, // optional
// default: process.argv.slice(2)
strict?: boolean, // optional, default `true`
allowPositionals?: boolean, // optional, default `false`
};.args:要解析的参数。如果我们省略此属性,则 parseArgs() 将使用 process.argv,从索引 2 处的元素开始。.strict:如果为 true,则如果 args 不正确,则会抛出异常。稍后会详细介绍。.allowPositionals:args 是否可以包含位置参数?这是 parseArgs() 结果的类型
type ParseArgsResult = {
values: {[key: string]: ValuesValue}, // an object
positionals: Array<string>, // always an Array
};
type ValuesValue = boolean | string | Array<boolean|string>;.values 包含可选参数。我们已经看到字符串和布尔值作为属性值。当我们探索 .multiple 为 true 的选项定义时,我们将看到数组值的属性。.positionals 包含位置参数。两个连字符用于引用选项的长版本。一个连字符用于引用简短版本
assert.deepEqual(
parseArgs({options, args: ['-v', '-c', 'green']}),
{
values: {__proto__:null,
verbose: true,
color: 'green',
},
positionals: []
}
);请注意,.values 包含选项的长名称。
我们在本小节的结尾解析与可选参数混合的位置参数
assert.deepEqual(
parseArgs({
options,
allowPositionals: true,
args: [
'home.html', '--verbose', 'main.js', '--color', 'red', 'post.md'
]
}),
{
values: {__proto__:null,
verbose: true,
color: 'red',
},
positionals: [
'home.html', 'main.js', 'post.md'
]
}
);如果我们多次使用一个选项,则默认情况下仅最后一次有效。它将覆盖所有先前的出现
const options = {
'bool': {
type: 'boolean',
},
'str': {
type: 'string',
},
};
assert.deepEqual(
parseArgs({
options, args: [
'--bool', '--bool', '--str', 'yes', '--str', 'no'
]
}),
{
values: {__proto__:null,
bool: true,
str: 'no'
},
positionals: []
}
);但是,如果我们在选项的定义中将 .multiple 设置为 true,则 parseArgs() 会在数组中为我们提供所有选项值
const options = {
'bool': {
type: 'boolean',
multiple: true,
},
'str': {
type: 'string',
multiple: true,
},
};
assert.deepEqual(
parseArgs({
options, args: [
'--bool', '--bool', '--str', 'yes', '--str', 'no'
]
}),
{
values: {__proto__:null,
bool: [ true, true ],
str: [ 'yes', 'no' ]
},
positionals: []
}
);请考虑以下选项
const options = {
'verbose': {
type: 'boolean',
short: 'v',
},
'silent': {
type: 'boolean',
short: 's',
},
'color': {
type: 'string',
short: 'c',
},
};以下是一种使用多个布尔选项的紧凑方法
assert.deepEqual(
parseArgs({options, args: ['-vs']}),
{
values: {__proto__:null,
verbose: true,
silent: true,
},
positionals: []
}
);我们可以通过等号直接附加长字符串选项的值。这称为*内联值*。
assert.deepEqual(
parseArgs({options, args: ['--color=green']}),
{
values: {__proto__:null,
color: 'green'
},
positionals: []
}
);短选项不能具有内联值。
到目前为止,所有选项值和位置值都是单个单词。如果我们要使用包含空格的值,则需要用双引号或单引号将其引起来。但是,并非所有 Shell 都支持后者。
为了检查 Shell 如何解析带引号的值,我们再次使用脚本 args.mjs
#!/usr/bin/env node
console.log(process.argv.slice(2));在 Unix 上,这些是双引号和单引号之间的区别
双引号:我们可以使用反斜杠(否则将按字面传递)转义引号,并且变量会被插值
% ./args.mjs "say \"hi\"" "\t\n" "$USER"
[ 'say "hi"', '\\t\\n', 'rauschma' ]单引号:所有内容都按字面传递,我们不能转义引号
% ./args.mjs 'back slash\' '\t\n' '$USER'
[ 'back slash\\', '\\t\\n', '$USER' ]以下交互演示了双引号和单引号的选项值
% ./args.mjs --str "two words" --str 'two words'
[ '--str', 'two words', '--str', 'two words' ]
% ./args.mjs --str="two words" --str='two words'
[ '--str=two words', '--str=two words' ]
% ./args.mjs -s "two words" -s 'two words'
[ '-s', 'two words', '-s', 'two words' ]
在 Windows 命令 Shell 中,单引号在任何方面都不是特殊的
>node args.mjs "say \"hi\"" "\t\n" "%USERNAME%"
[ 'say "hi"', '\\t\\n', 'jane' ]
>node args.mjs 'back slash\' '\t\n' '%USERNAME%'
[ "'back", "slash\\'", "'\\t\\n'", "'jane'" ]
Windows 命令 Shell 中带引号的选项值
>node args.mjs --str 'two words' --str "two words"
[ '--str', "'two", "words'", '--str', 'two words' ]
>node args.mjs --str='two words' --str="two words"
[ "--str='two", "words'", '--str=two words' ]
>>node args.mjs -s "two words" -s 'two words'
[ '-s', 'two words', '-s', "'two", "words'" ]
在 Windows PowerShell 中,我们可以使用单引号进行引用,变量名不会在引号内插值,并且单引号不能转义
> node args.mjs "say `"hi`"" "\t\n" "%USERNAME%"
[ 'say hi', '\\t\\n', '%USERNAME%' ]
> node args.mjs 'backtick`' '\t\n' '%USERNAME%'
[ 'backtick`', '\\t\\n', '%USERNAME%' ]
parseArgs() 如何处理带引号的值以下是 parseArgs() 处理带引号的值的方式
const options = {
'times': {
type: 'string',
short: 't',
},
'color': {
type: 'string',
short: 'c',
},
};
// Quoted external option values
assert.deepEqual(
parseArgs({
options,
args: ['-t', '5 times', '--color', 'light green']
}),
{
values: {__proto__:null,
times: '5 times',
color: 'light green',
},
positionals: []
}
);
// Quoted inline option values
assert.deepEqual(
parseArgs({
options,
args: ['--color=light green']
}),
{
values: {__proto__:null,
color: 'light green',
},
positionals: []
}
);
// Quoted positional values
assert.deepEqual(
parseArgs({
options, allowPositionals: true,
args: ['two words', 'more words']
}),
{
values: {__proto__:null,
},
positionals: [ 'two words', 'more words' ]
}
);parseArgs() 支持所谓的*选项终止符*:如果 args 的元素之一是双连字符 (--),则其余参数都将被视为位置参数。
哪里需要选项终止符?一些可执行文件会调用其他可执行文件,例如node 可执行文件。然后可以使用选项终止符来分隔调用者的参数和被调用者的参数。
以下是 parseArgs() 处理选项终止符的方式
const options = {
'verbose': {
type: 'boolean',
},
'count': {
type: 'string',
},
};
assert.deepEqual(
parseArgs({options, allowPositionals: true,
args: [
'how', '--verbose', 'are', '--', '--count', '5', 'you'
]
}),
{
values: {__proto__:null,
verbose: true
},
positionals: [ 'how', 'are', '--count', '5', 'you' ]
}
);parseArgs()如果选项 .strict 为 true(默认值),则如果发生以下情况之一,parseArgs() 将抛出异常
args 中使用的选项名称不在 options 中。args 中的选项类型错误。目前,仅当字符串选项缺少参数时才会发生这种情况。.allowPositions 为 false(默认值),args 中也存在位置参数。以下代码演示了每种情况
const options = {
'str': {
type: 'string',
},
};
// Unknown option name
assert.throws(
() => parseArgs({
options,
args: ['--unknown']
}),
{
name: 'TypeError',
message: "Unknown option '--unknown'",
}
);
// Wrong option type (missing value)
assert.throws(
() => parseArgs({
options,
args: ['--str']
}),
{
name: 'TypeError',
message: "Option '--str <value>' argument missing",
}
);
// Unallowed positional
assert.throws(
() => parseArgs({
options,
allowPositionals: false, // (the default)
args: ['posarg']
}),
{
name: 'TypeError',
message: "Unexpected argument 'posarg'. " +
"This command does not take positional arguments",
}
);parseArgs 令牌parseArgs() 分两个阶段处理 args 数组
args 解析为令牌数组:这些令牌主要是用类型信息注释的 args 元素:它是选项吗?它是位置参数吗?等等。但是,如果一个选项有一个值,那么令牌会同时存储选项名称和选项值,因此包含两个 args 元素的数据。.values 返回的对象中。如果我们将 config.tokens 设置为 true,则可以访问令牌。然后,parseArgs() 返回的对象包含一个属性 .tokens,其中包含令牌。
这些是令牌的属性
type Token = OptionToken | PositionalToken | OptionTerminatorToken;
interface CommonTokenProperties {
/** Where in `args` does the token start? */
index: number;
}
interface OptionToken extends CommonTokenProperties {
kind: 'option';
/** Long name of option */
name: string;
/** The option name as mentioned in `args` */
rawName: string;
/** The option’s value. `undefined` for boolean options. */
value: string | undefined;
/** Is the option value specified inline (e.g. --level=5)? */
inlineValue: boolean | undefined;
}
interface PositionalToken extends CommonTokenProperties {
kind: 'positional';
/** The value of the positional, args[token.index] */
value: string;
}
interface OptionTerminatorToken extends CommonTokenProperties {
kind: 'option-terminator';
}例如,请考虑以下选项
const options = {
'bool': {
type: 'boolean',
short: 'b',
},
'flag': {
type: 'boolean',
short: 'f',
},
'str': {
type: 'string',
short: 's',
},
};布尔选项的令牌如下所示
assert.deepEqual(
parseArgs({
options, tokens: true,
args: [
'--bool', '-b', '-bf',
]
}),
{
values: {__proto__:null,
bool: true,
flag: true,
},
positionals: [],
tokens: [
{
kind: 'option',
name: 'bool',
rawName: '--bool',
index: 0,
value: undefined,
inlineValue: undefined
},
{
kind: 'option',
name: 'bool',
rawName: '-b',
index: 1,
value: undefined,
inlineValue: undefined
},
{
kind: 'option',
name: 'bool',
rawName: '-b',
index: 2,
value: undefined,
inlineValue: undefined
},
{
kind: 'option',
name: 'flag',
rawName: '-f',
index: 2,
value: undefined,
inlineValue: undefined
},
]
}
);请注意,选项 bool 有三个令牌,因为它在 args 中被提到了三次。但是,由于解析的第二阶段,.values 中只有一个 bool 属性。
在下一个示例中,我们将字符串选项解析为令牌。.inlineValue 现在具有布尔值(对于布尔选项,它始终为 undefined)
assert.deepEqual(
parseArgs({
options, tokens: true,
args: [
'--str', 'yes', '--str=yes', '-s', 'yes',
]
}),
{
values: {__proto__:null,
str: 'yes',
},
positionals: [],
tokens: [
{
kind: 'option',
name: 'str',
rawName: '--str',
index: 0,
value: 'yes',
inlineValue: false
},
{
kind: 'option',
name: 'str',
rawName: '--str',
index: 2,
value: 'yes',
inlineValue: true
},
{
kind: 'option',
name: 'str',
rawName: '-s',
index: 3,
value: 'yes',
inlineValue: false
}
]
}
);最后,这是一个解析位置参数和选项终止符的示例
assert.deepEqual(
parseArgs({
options, allowPositionals: true, tokens: true,
args: [
'command', '--', '--str', 'yes', '--str=yes'
]
}),
{
values: {__proto__:null,
},
positionals: [ 'command', '--str', 'yes', '--str=yes' ],
tokens: [
{ kind: 'positional', index: 0, value: 'command' },
{ kind: 'option-terminator', index: 1 },
{ kind: 'positional', index: 2, value: '--str' },
{ kind: 'positional', index: 3, value: 'yes' },
{ kind: 'positional', index: 4, value: '--str=yes' }
]
}
);默认情况下,parseArgs() 不支持子命令,例如 git clone 或 npm install。但是,通过令牌实现此功能相对容易。
这是实现
function parseSubcommand(config) {
// The subcommand is a positional, allow them
const {tokens} = parseArgs({
...config, tokens: true, allowPositionals: true
});
let firstPosToken = tokens.find(({kind}) => kind==='positional');
if (!firstPosToken) {
throw new Error('Command name is missing: ' + config.args);
}
//----- Command options
const cmdArgs = config.args.slice(0, firstPosToken.index);
// Override `config.args`
const commandResult = parseArgs({
...config, args: cmdArgs, tokens: false, allowPositionals: false
});
//----- Subcommand
const subcommandName = firstPosToken.value;
const subcmdArgs = config.args.slice(firstPosToken.index+1);
// Override `config.args`
const subcommandResult = parseArgs({
...config, args: subcmdArgs, tokens: false
});
return {
commandResult,
subcommandName,
subcommandResult,
};
}这是 parseSubcommand() 的实际应用
const options = {
'log': {
type: 'string',
},
color: {
type: 'boolean',
}
};
const args = ['--log', 'all', 'print', '--color', 'file.txt'];
const result = parseSubcommand({options, allowPositionals: true, args});
const pn = obj => Object.setPrototypeOf(obj, null);
assert.deepEqual(
result,
{
commandResult: {
values: pn({'log': 'all'}),
positionals: []
},
subcommandName: 'print',
subcommandResult: {
values: pn({color: true}),
positionals: ['file.txt']
}
}
);