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 = {
: 'boolean' | 'string', // required
type?: string, // optional
short?: boolean, // optional, default `false`
multiple; }
.type
指定选项是布尔值还是字符串。.short
定义选项的简短版本。它必须是单个字符。我们很快就会看到如何使用简短版本。.multiple
指示一个选项最多可以使用一次还是可以使用零次或多次。我们稍后会看到这意味着什么。以下代码使用 parseArgs()
和 options
来解析带有参数的数组
.deepEqual(
assertparseArgs({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 = {
?: {[key: string], Options}, // optional, default: {}
options?: Array<string>, // optional
args// default: process.argv.slice(2)
?: boolean, // optional, default `true`
strict?: boolean, // optional, default `false`
allowPositionals; }
.args
:要解析的参数。如果我们省略此属性,则 parseArgs()
将使用 process.argv
,从索引 2 处的元素开始。.strict
:如果为 true
,则如果 args
不正确,则会抛出异常。稍后会详细介绍。.allowPositionals
:args
是否可以包含位置参数?这是 parseArgs()
结果的类型
type ParseArgsResult = {
: {[key: string]: ValuesValue}, // an object
values: Array<string>, // always an Array
positionals;
}type ValuesValue = boolean | string | Array<boolean|string>;
.values
包含可选参数。我们已经看到字符串和布尔值作为属性值。当我们探索 .multiple
为 true
的选项定义时,我们将看到数组值的属性。.positionals
包含位置参数。两个连字符用于引用选项的长版本。一个连字符用于引用简短版本
.deepEqual(
assertparseArgs({options, args: ['-v', '-c', 'green']}),
{values: {__proto__:null,
verbose: true,
color: 'green',
,
}positionals: []
}; )
请注意,.values
包含选项的长名称。
我们在本小节的结尾解析与可选参数混合的位置参数
.deepEqual(
assertparseArgs({
,
optionsallowPositionals: 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',
,
};
}
.deepEqual(
assertparseArgs({
, args: [
options'--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,
,
};
}
.deepEqual(
assertparseArgs({
, args: [
options'--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',
,
}; }
以下是一种使用多个布尔选项的紧凑方法
.deepEqual(
assertparseArgs({options, args: ['-vs']}),
{values: {__proto__:null,
verbose: true,
silent: true,
,
}positionals: []
}; )
我们可以通过等号直接附加长字符串选项的值。这称为*内联值*。
.deepEqual(
assertparseArgs({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
.deepEqual(
assertparseArgs({
,
optionsargs: ['-t', '5 times', '--color', 'light green']
,
})
{values: {__proto__:null,
times: '5 times',
color: 'light green',
,
}positionals: []
};
)
// Quoted inline option values
.deepEqual(
assertparseArgs({
,
optionsargs: ['--color=light green']
,
})
{values: {__proto__:null,
color: 'light green',
,
}positionals: []
};
)
// Quoted positional values
.deepEqual(
assertparseArgs({
, allowPositionals: true,
optionsargs: ['two words', 'more words']
,
})
{values: {__proto__:null,
,
}positionals: [ 'two words', 'more words' ]
}; )
parseArgs()
支持所谓的*选项终止符*:如果 args
的元素之一是双连字符 (--
),则其余参数都将被视为位置参数。
哪里需要选项终止符?一些可执行文件会调用其他可执行文件,例如node
可执行文件。然后可以使用选项终止符来分隔调用者的参数和被调用者的参数。
以下是 parseArgs()
处理选项终止符的方式
const options = {
'verbose': {
type: 'boolean',
,
}'count': {
type: 'string',
,
};
}
.deepEqual(
assertparseArgs({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
.throws(
assert=> parseArgs({
() ,
optionsargs: ['--unknown']
,
})
{name: 'TypeError',
message: "Unknown option '--unknown'",
};
)
// Wrong option type (missing value)
.throws(
assert=> parseArgs({
() ,
optionsargs: ['--str']
,
})
{name: 'TypeError',
message: "Option '--str <value>' argument missing",
};
)
// Unallowed positional
.throws(
assert=> parseArgs({
() ,
optionsallowPositionals: 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? */
: number;
index
}
interface OptionToken extends CommonTokenProperties {
: 'option';
kind
/** Long name of option */
: string;
name
/** The option name as mentioned in `args` */
: string;
rawName
/** The option’s value. `undefined` for boolean options. */
: string | undefined;
value
/** Is the option value specified inline (e.g. --level=5)? */
: boolean | undefined;
inlineValue
}
interface PositionalToken extends CommonTokenProperties {
: 'positional';
kind
/** The value of the positional, args[token.index] */
: string;
value
}
interface OptionTerminatorToken extends CommonTokenProperties {
: 'option-terminator';
kind }
例如,请考虑以下选项
const options = {
'bool': {
type: 'boolean',
short: 'b',
,
}'flag': {
type: 'boolean',
short: 'f',
,
}'str': {
type: 'string',
short: 's',
,
}; }
布尔选项的令牌如下所示
.deepEqual(
assertparseArgs({
, tokens: true,
optionsargs: [
'--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
)
.deepEqual(
assertparseArgs({
, tokens: true,
optionsargs: [
'--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
}
]
}; )
最后,这是一个解析位置参数和选项终止符的示例
.deepEqual(
assertparseArgs({
, allowPositionals: true, tokens: true,
optionsargs: [
'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);
.deepEqual(
assert,
result
{commandResult: {
values: pn({'log': 'all'}),
positionals: []
,
}subcommandName: 'print',
subcommandResult: {
values: pn({color: true}),
positionals: ['file.txt']
}
}; )