在Node.js中用util.parseArgs()解析命令行参数的教程

1,384 阅读9分钟

在这篇博文中,我们探讨了如何使用模块node:util 中的Node.js函数parseArgs() 来解析命令行参数。


本博文中隐含的导入项

本博文的每个例子中都隐含了以下两个导入。

import * as assert from 'node:assert/strict';
import {parseArgs} from 'node:util';

第一个导入是用于测试断言,我们用它来检查数值。第二个导入是用于函数parseArgs() ,这是本帖的主题。

处理命令行参数的步骤

以下是处理命令行参数的步骤:

  1. 用户输入一个文本字符串。
  2. shell将该字符串解析为一连串的单词和运算符。
  3. 如果一个命令被调用,它就会得到零个或多个单词作为参数。
  4. 我们的Node.js代码通过一个存储在process.argv 的数组接收单词。 process是Node.js的一个全局变量。
  5. 我们使用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.htmlmain.js

使用参数的两种风格很常见:

  • 主要参数是位置性的,选项提供额外的--通常是可选的--信息。
  • 只有选项被使用。

写成JavaScript函数调用,前面的例子看起来是这样的(在JavaScript中,选项通常放在最后)。

argsMjs('home.html', 'main.js', {str: 'abc', bool: false});

基础知识

如果我们想让parseArgs() 来解析一个带有参数的数组,我们首先需要告诉它我们的选项是如何工作的。让我们假设我们的脚本有:

  • 一个布尔型选项--verbose
  • 一个接收非负整数的选项--timesparseArgs() 没有对数字的特殊支持,所以我们必须把它变成一个字符串选项。
  • 一个字符串选项--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 指定一个选项是布尔值还是字符串。
  • .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

如前所述,数字5是--times 的值,被处理成一个字符串。

我们传递给parseArgs() 的对象具有以下 TypeScript 类型。

  • .args:要解析的参数。如果我们省略这个属性,parseArgs() 使用process.argv ,从索引2的元素开始。
  • .strict:如果true ,如果args 不正确,就会抛出一个异常。稍后会有更多关于这个的内容。
  • .allowPositionals:args 能否包含位置参数?

这就是parseArgs() 的结果类型。

  • .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都支持后者。

shells如何解析带引号的值

为了研究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 Command 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命令壳中的带引号的选项值。

>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() 处理带引号的值的方法。

选项终结符

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()

如果选项.stricttrue (这是默认的),那么如果发生下列情况之一,parseArgs() 会抛出一个异常。

  • args 中使用的选项名称不在options 中。
  • args 中的一个选项有错误的类型。目前,只有在字符串选项缺少一个参数时才会发生这种情况。
  • args 中有位置参数,尽管.allowPositionsfalse (这是默认的)。

下面的代码分别演示了这些情况。

parseArgs tokens

parseArgs() 分两个阶段处理args 数组。

  • 第一阶段:它将args 解析为一个令牌数组。这些令牌主要是args 的元素,并注有类型信息。它是一个选项吗?它是一个位置信息吗?等等。然而,如果一个选项有一个值,那么这个标记同时存储了选项名称和选项值,因此包含了两个args 元素的数据。
  • 第二阶段:它将令牌组装成对象,通过结果属性返回.values

如果我们将config.tokens 设置为true ,就可以获得令牌的访问权。然后由parseArgs() 返回的对象包含一个带有令牌的属性.tokens

这些是令牌的属性。

代币的例子

作为一个例子,考虑下面的选项。

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 clonenpm install 这样的子命令。然而,通过令牌来实现这一功能是比较容易的。

这就是实现方法。

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