Node.js によるシェルスクリプト
本書のオフライン版(HTML、PDF、EPUB、MOBI)を購入して、無料オンライン版をサポートしてください。
(広告です。ブロックしないでください。)

16 util.parseArgs() を使ったコマンドライン引数の解析



この章では、モジュール node:util の Node.js 関数 parseArgs() を使用してコマンドライン引数を解析する方法を説明します。

16.1 この章で暗黙的に使用するインポート

この章のすべての例では、以下の2つのインポートが暗黙的に使用されています。

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

最初のインポートは、値をチェックするために使用するテストアサーション用です。2番目のインポートは、この章のトピックである関数 parseArgs() 用です。

16.2 コマンドライン引数の処理に関わる手順

コマンドライン引数の処理には、以下の手順が含まれます。

  1. ユーザーがテキスト文字列を入力します。
  2. シェルが文字列を単語と演算子のシーケンスに解析します。
  3. コマンドが呼び出されると、0個以上の単語が引数として渡されます。
  4. Node.js コードは、process.argv に格納された配列を介して単語を受け取ります。 process は Node.js のグローバル変数です。
  5. parseArgs() を使用して、その配列をより扱いやすいものに変換します。

Node.js コードを含む次のシェルスクリプト 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 コマンドシェルでは同じコマンドで次の結果が得られます。

[
  'C:\\Program Files\\nodejs\\node.exe',
  'C:\\Users\\jane\\args.mjs',
  'one',
  'two'
]

シェルスクリプトをどのように呼び出すかにかかわらず、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' ]

これらの引数は以下で構成されています。

引数の使用には 2 つの一般的なスタイルがあります。

前の例を JavaScript 関数呼び出しとして記述すると、次のようになります(JavaScript では、通常、オプションは最後に配置されます)。

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

16.3 コマンドライン引数の解析

16.3.1 基本

parseArgs() に引数を含む配列を解析させるには、まずオプションの動作を伝える必要があります。スクリプトに以下があると仮定します。

これらのオプションを 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 = {
  type: 'boolean' | 'string', // required
  short?: string, // optional
  multiple?: boolean, // optional, default `false`
};

次のコードでは、parseArgs()options を使用して引数を含む配列を解析します。

assert.deepEqual(
  parseArgs({options, args: [
    '--verbose', '--color', 'green', '--times', '5'
  ]}),
  {
    values: {__proto__:null,
      verbose: true,
      color: 'green',
      times: '5'
    },
    positionals: []
  }
);

.values に格納されたオブジェクトのプロトタイプは null です。つまり、.toString などの継承されたプロパティを気にすることなく、in 演算子を使用してプロパティが存在するかどうかを確認できます。

前述したように、--times の値である数値 5 は、文字列として処理されます。

parseArgs() に渡すオブジェクトには、次の TypeScript 型があります。

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

これは parseArgs() の結果の型です。

type ParseArgsResult = {
  values: {[key: string]: ValuesValue}, // an object
  positionals: Array<string>, // always an Array
};
type ValuesValue = boolean | string | Array<boolean|string>;

2 つのハイフンはオプションのロングバージョンを参照するために使用されます。1 つのハイフンはショートバージョンを参照するために使用されます。

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

16.3.2 オプションの複数回使用

オプションを複数回使用する場合、デフォルトでは、最後の回のみがカウントされます。以前のすべての出現を上書きします。

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

ただし、オプションの定義で .multipletrue に設定すると、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: []
  }
);

16.3.3 ロングオプションとショートオプションのその他の使い方

次のオプションを検討してください。

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

ショートオプションにはインライン値を設定できません。

16.3.4 値のクォート

これまでのところ、すべてのオプション値と位置値は単語でした。スペースを含む値を使用する場合は、二重引用符または一重引用符で囲む必要があります。ただし、後者はすべてのシェルでサポートされているわけではありません。

16.3.4.1 シェルが引用符付きの値を解析する方法

シェルが引用符付きの値を解析する方法を調べるために、スクリプト args.mjs を再度使用します。

#!/usr/bin/env node
console.log(process.argv.slice(2));

Unix では、二重引用符と一重引用符の間には次の違いがあります。

次のやり取りは、二重引用符と一重引用符で囲まれたオプションの値を示しています。

% ./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%' ]
16.3.4.2 parseArgs() が引用符付きの値を処理する方法

parseArgs() が引用符付きの値を処理する方法を以下に示します。

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 オプションターミネータ

parseArgs() は、いわゆる*オプションターミネータ*をサポートしています。args の要素の 1 つが二重ハイフン (--) である場合、残りの引数はすべて位置引数として扱われます。

オプションターミネータはどこで必要になりますか。一部の実行可能ファイルは他の実行可能ファイルを呼び出します。例: 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' ]
  }
);

16.3.6 厳格な parseArgs()

オプション .stricttrue(デフォルト)の場合、次のいずれかが発生すると、parseArgs() は例外をスローします。

次のコードは、これらの各ケースを示しています。

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 トークン

parseArgs() は、args 配列を 2 つのフェーズで処理します。

config.tokenstrue に設定すると、トークンにアクセスできます。次に、parseArgs() によって返されるオブジェクトには、トークンを持つプロパティ .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 トークンの例

例として、次のオプションを検討してください。

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

args で 3 回言及されているため、オプション bool のトークンが 3 つあることに注意してください。ただし、解析のフェーズ 2 のため、.valuesbool のプロパティは 1 つしかありません。

次の例では、文字列オプションをトークンに解析します。.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' }
    ]
  }
);

16.4.2 トークンを使用したサブコマンドの実装

デフォルトでは、parseArgs()git clonenpm 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);
assert.deepEqual(
  result,
  {
    commandResult: {
      values: pn({'log': 'all'}),
      positionals: []
    },
    subcommandName: 'print',
    subcommandResult: {
      values: pn({color: true}),
      positionals: ['file.txt']
    }
  }
);