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' ]
如果我們透過 npm 在 Windows 上安裝命令,則相同的命令會在 Windows 命令殼層中產生以下結果
['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
。
如前所述,數字 5 是 --times
的值,會被視為字串處理。
我們傳遞給 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 命令殼層中,單引號在任何方面都不是特殊的
>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 命令殼層中的帶引號選項值
>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
中的選項類型錯誤。目前,只有當字串選項缺少引數時才會發生這種情況。args
中有位置引數,即使 .allowPositions
為 false
(這是預設值)。以下程式碼示範了每個案例
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']
}
}; )