util.parseArgs() parsenparseArgs()parseArgs-TokensIn diesem Kapitel untersuchen wir, wie die Node.js-Funktion parseArgs() aus dem Modul node:util zum Parsen von Kommandozeilenargumenten verwendet wird.
Die folgenden beiden Imports werden in jedem Beispiel in diesem Kapitel vorausgesetzt
import * as assert from 'node:assert/strict';
import {parseArgs} from 'node:util';Der erste Import dient Test-Assertions, die wir zur Überprüfung von Werten verwenden. Der zweite Import gilt der Funktion parseArgs(), dem Thema dieses Kapitels.
Die folgenden Schritte sind bei der Verarbeitung von Kommandozeilenargumenten beteiligt
process.argv gespeichert ist. process ist eine globale Variable in Node.js.parseArgs(), um dieses Array in etwas umzuwandeln, das einfacher zu handhaben ist.Verwenden wir das folgende Shell-Skript args.mjs mit Node.js-Code, um zu sehen, wie process.argv aussieht
#!/usr/bin/env node
console.log(process.argv);Wir beginnen mit einem einfachen Befehl
% ./args.mjs one two
[ '/usr/bin/node', '/home/john/args.mjs', 'one', 'two' ]
Wenn wir den Befehl unter Windows über npm installieren, liefert derselbe Befehl unter der Windows-Kommandozeile das folgende Ergebnis
[
'C:\\Program Files\\nodejs\\node.exe',
'C:\\Users\\jane\\args.mjs',
'one',
'two'
]Unabhängig davon, wie wir ein Shell-Skript aufrufen, beginnt process.argv immer mit dem Pfad der Node.js-Binärdatei, die zum Ausführen unseres Codes verwendet wird. Danach folgt der Pfad unseres Skripts. Das Array endet mit den tatsächlichen Argumenten, die dem Skript übergeben wurden. Mit anderen Worten: Die Argumente eines Skripts beginnen immer bei Index 2.
Daher ändern wir unser Skript wie folgt ab
#!/usr/bin/env node
console.log(process.argv.slice(2));Probieren wir kompliziertere Argumente
% ./args.mjs --str abc --bool home.html main.js
[ '--str', 'abc', '--bool', 'home.html', 'main.js' ]
Diese Argumente bestehen aus
--str, deren Wert der Text abc ist. Eine solche Option wird als String-Option bezeichnet.--bool, die keinen zugehörigen Wert hat – sie ist ein Flag, das entweder vorhanden ist oder nicht. Eine solche Option wird als Boolean-Option bezeichnet.home.html und main.js.Zwei gängige Arten, Argumente zu verwenden
Als JavaScript-Funktionsaufruf geschrieben, würde das vorherige Beispiel wie folgt aussehen (in JavaScript kommen Optionen normalerweise zuletzt)
argsMjs('home.html', 'main.js', {str: 'abc', bool: false});Wenn wir möchten, dass parseArgs() ein Array mit Argumenten parst, müssen wir ihm zuerst mitteilen, wie unsere Optionen funktionieren. Nehmen wir an, unser Skript hat
--verbose--times, die nicht-negative Ganzzahlen empfängt. parseArgs() hat keine spezielle Unterstützung für Zahlen, daher müssen wir sie als String-Option behandeln.--colorWir beschreiben diese Optionen für parseArgs() wie folgt
const options = {
'verbose': {
type: 'boolean',
short: 'v',
},
'color': {
type: 'string',
short: 'c',
},
'times': {
type: 'string',
short: 't',
},
};Solange ein Eigenschaftsschlüssel von options ein gültiger JavaScript-Bezeichner ist, liegt es Ihnen, ob Sie ihn in Anführungszeichen setzen möchten oder nicht. Beides hat Vor- und Nachteile. In diesem Kapitel werden sie immer in Anführungszeichen gesetzt. So sehen Optionen mit Nicht-Bezeichner-Namen wie my-new-option genauso aus wie solche mit Bezeichner-Namen.
Jeder Eintrag in options kann die folgenden Eigenschaften haben (wie über einen TypeScript-Typ definiert)
type Options = {
type: 'boolean' | 'string', // required
short?: string, // optional
multiple?: boolean, // optional, default `false`
};.type gibt an, ob eine Option boolean oder string ist..short definiert die Kurzversion einer Option. Sie muss ein einzelnes Zeichen sein. Wie wir Kurzversionen verwenden, sehen wir bald..multiple gibt an, ob eine Option höchstens einmal oder null- oder mehrmals verwendet werden kann. Was das bedeutet, sehen wir später.Der folgende Code verwendet parseArgs() und options, um ein Array mit Argumenten zu parsen
assert.deepEqual(
parseArgs({options, args: [
'--verbose', '--color', 'green', '--times', '5'
]}),
{
values: {__proto__:null,
verbose: true,
color: 'green',
times: '5'
},
positionals: []
}
);Der Prototyp des Objekts, das in .values gespeichert ist, ist null. Das bedeutet, dass wir den in-Operator verwenden können, um zu prüfen, ob eine Eigenschaft existiert, ohne uns um geerbte Eigenschaften wie .toString sorgen zu müssen.
Wie bereits erwähnt, wird die Zahl 5, der Wert von --times, als String verarbeitet.
Das Objekt, das wir an parseArgs() übergeben, hat den folgenden TypeScript-Typ
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: Die zu parsenden Argumente. Wenn wir diese Eigenschaft weglassen, verwendet parseArgs() process.argv, beginnend mit dem Element an Index 2..strict: Wenn true, wird eine Ausnahme ausgelöst, wenn args nicht korrekt ist. Mehr dazu später..allowPositionals: Können args positionelle Argumente enthalten?Das ist der Typ des Ergebnisses von parseArgs()
type ParseArgsResult = {
values: {[key: string]: ValuesValue}, // an object
positionals: Array<string>, // always an Array
};
type ValuesValue = boolean | string | Array<boolean|string>;.values enthält die optionalen Argumente. Wir haben bereits Strings und Booleans als Eigenschaftswerte gesehen. Array-Werte erhalten wir bei der Untersuchung von Optionsdefinitionen, bei denen .multiple auf true gesetzt ist..positionals enthält die positionellen Argumente.Zwei Bindestriche werden verwendet, um auf die lange Version einer Option zu verweisen. Ein Bindestrich wird verwendet, um auf die kurze Version zu verweisen
assert.deepEqual(
parseArgs({options, args: ['-v', '-c', 'green']}),
{
values: {__proto__:null,
verbose: true,
color: 'green',
},
positionals: []
}
);Beachten Sie, dass .values die langen Namen der Optionen enthält.
Wir schließen diesen Unterabschnitt ab, indem wir positionelle Argumente parsen, die mit optionalen Argumenten vermischt sind
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'
]
}
);Wenn wir eine Option mehrmals verwenden, zählt standardmäßig nur das letzte Mal. Es überschreibt alle vorherigen Vorkommen
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: []
}
);Wenn wir jedoch .multiple in der Definition einer Option auf true setzen, gibt parseArgs() uns alle Optionswerte in einem Array zurück
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: []
}
);Betrachten Sie die folgenden Optionen
const options = {
'verbose': {
type: 'boolean',
short: 'v',
},
'silent': {
type: 'boolean',
short: 's',
},
'color': {
type: 'string',
short: 'c',
},
};Die folgende Schreibweise ist eine kompakte Methode, mehrere Boolean-Optionen zu verwenden
assert.deepEqual(
parseArgs({options, args: ['-vs']}),
{
values: {__proto__:null,
verbose: true,
silent: true,
},
positionals: []
}
);Wir können den Wert einer langen String-Option direkt über ein Gleichheitszeichen anhängen. Das nennt man einen Inline-Wert.
assert.deepEqual(
parseArgs({options, args: ['--color=green']}),
{
values: {__proto__:null,
color: 'green'
},
positionals: []
}
);Kurze Optionen können keine Inline-Werte haben.
Bisher waren alle Optionswerte und positionellen Werte einzelne Wörter. Wenn wir Werte verwenden möchten, die Leerzeichen enthalten, müssen wir sie in Anführungszeichen setzen – mit doppelten oder einfachen Anführungszeichen. Letztere werden jedoch nicht von allen Shells unterstützt.
Um zu untersuchen, wie Shells Werte in Anführungszeichen parsen, verwenden wir erneut das Skript args.mjs
#!/usr/bin/env node
console.log(process.argv.slice(2));Unter Unix ergeben sich folgende Unterschiede zwischen doppelten und einfachen Anführungszeichen
Doppelte Anführungszeichen: Wir können Anführungszeichen mit Backslashes escapen (die ansonsten unverändert weitergegeben werden) und Variablen werden interpoliert
% ./args.mjs "say \"hi\"" "\t\n" "$USER"
[ 'say "hi"', '\\t\\n', 'rauschma' ]Einfache Anführungszeichen: Alle Inhalte werden unverändert weitergegeben und wir können keine Anführungszeichen escapen
% ./args.mjs 'back slash\' '\t\n' '$USER'
[ 'back slash\\', '\\t\\n', '$USER' ]Die folgende Interaktion demonstriert Optionswerte, die doppelt und einfach in Anführungszeichen gesetzt sind
% ./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' ]
In der Windows-Kommandozeile haben einfache Anführungszeichen keine besondere Bedeutung
>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'" ]
Optionswerte in Anführungszeichen in der Windows-Kommandozeile
>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'" ]
In Windows PowerShell können wir einfache Anführungszeichen verwenden, Variablennamen werden innerhalb von Anführungszeichen nicht interpoliert und einfache Anführungszeichen können nicht escapet werden
> 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() Werte in Anführungszeichen behandeltSo behandelt parseArgs() Werte in Anführungszeichen
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() unterstützt sogenannte Options-Terminatoren: Wenn eines der Elemente von args ein doppeltes Bindestrich (--) ist, werden alle verbleibenden Argumente als positionell behandelt.
Wo sind Options-Terminatoren erforderlich? Einige ausführbare Programme rufen andere ausführbare Programme auf, z. B. das node-Executable. Dann kann ein Options-Terminator verwendet werden, um die Argumente des Aufrufers von den Argumenten des Aufgerufenen zu trennen.
So behandelt parseArgs() Options-Terminatoren
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()Wenn die Option .strict auf true gesetzt ist (was der Standard ist), löst parseArgs() eine Ausnahme aus, wenn eines der folgenden Dinge passiert
args verwendet wird, ist nicht in options enthalten.args hat den falschen Typ. Derzeit geschieht dies nur, wenn einer String-Option ein Argument fehlt.args, obwohl .allowPositions auf false gesetzt ist (was der Standard ist).Der folgende Code demonstriert jeden dieser Fälle
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-TokensparseArgs() verarbeitet das args-Array in zwei Phasen
args in ein Array von Tokens: Diese Tokens sind größtenteils die Elemente von args, annotiert mit Typinformationen: Ist es eine Option? Ist es positionell? usw. Wenn eine Option jedoch einen Wert hat, speichert das Token sowohl den Optionsnamen als auch den Optionswert und enthält daher die Daten von zwei args-Elementen..values zurückgegeben wird.Wir können auf die Tokens zugreifen, wenn wir config.tokens auf true setzen. Dann enthält das von parseArgs() zurückgegebene Objekt eine Eigenschaft .tokens mit den Tokens.
Das sind die Eigenschaften von 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';
}Als Beispiel betrachten wir die folgenden Optionen
const options = {
'bool': {
type: 'boolean',
short: 'b',
},
'flag': {
type: 'boolean',
short: 'f',
},
'str': {
type: 'string',
short: 's',
},
};Die Tokens für Boolean-Optionen sehen wie folgt aus
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
},
]
}
);Beachten Sie, dass es drei Tokens für die Option bool gibt, weil sie dreimal in args erwähnt wird. Aufgrund der Phase 2 des Parsens gibt es jedoch nur eine Eigenschaft für bool in .values.
Im nächsten Beispiel parsen wir String-Optionen in Tokens. .inlineValue hat jetzt boolesche Werte (es ist immer undefined für Boolean-Optionen)
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
}
]
}
);Zuletzt ist dies ein Beispiel für das Parsen von positionellen Argumenten und einem Options-Terminator
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' }
]
}
);Standardmäßig unterstützt parseArgs() keine Unterbefehle wie git clone oder npm install. Es ist jedoch relativ einfach, diese Funktionalität über Tokens zu implementieren.
Dies ist die Implementierung
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,
};
}Das ist parseSubcommand() in Aktion
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']
}
}
);