Shell-Skripting mit Node.js
Sie können die Offline-Version dieses Buches (HTML, PDF, EPUB, MOBI) kaufen und damit die kostenlose Online-Version unterstützen.
(Werbung, bitte nicht blockieren.)

16 Kommandozeilenargumente mit util.parseArgs() parsen



In diesem Kapitel untersuchen wir, wie die Node.js-Funktion parseArgs() aus dem Modul node:util zum Parsen von Kommandozeilenargumenten verwendet wird.

16.1 Imports, die in diesem Kapitel vorausgesetzt werden

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.

16.2 Die Schritte bei der Verarbeitung von Kommandozeilenargumenten

Die folgenden Schritte sind bei der Verarbeitung von Kommandozeilenargumenten beteiligt

  1. Der Benutzer gibt eine Zeichenkette ein.
  2. Die Shell zerlegt die Zeichenkette in eine Sequenz von Wörtern und Operatoren.
  3. Wenn ein Befehl aufgerufen wird, erhält er null oder mehr Wörter als Argumente.
  4. Unser Node.js-Code empfängt die Wörter über ein Array, das in process.argv gespeichert ist. process ist eine globale Variable in Node.js.
  5. Wir verwenden 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

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});

16.3 Kommandozeilenargumente parsen

16.3.1 Die Grundlagen

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

Wir 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`
};

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`
};

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>;

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'
    ]
  }
);

16.3.2 Optionen mehrmals verwenden

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: []
  }
);

16.3.3 Weitere Möglichkeiten, lange und kurze Optionen zu verwenden

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.

16.3.4 Werte in Anführungszeichen setzen

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.

16.3.4.1 Wie Shells Werte in Anführungszeichen parsen

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

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%' ]
16.3.4.2 Wie parseArgs() Werte in Anführungszeichen behandelt

So 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' ]
  }
);

16.3.5 Options-Terminatoren

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' ]
  }
);

16.3.6 Strenges 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

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",
  }
);

16.4 parseArgs-Tokens

parseArgs() verarbeitet das args-Array in zwei Phasen

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';
}

16.4.1 Beispiele für Tokens

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' }
    ]
  }
);

16.4.2 Tokens zur Implementierung von Unterbefehlen verwenden

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']
    }
  }
);