Node.jsでCLIの引数を解析する方法、node:util.parseArgsのご紹介も！

はいさい！ちゅらデータぬオースティンやいびーん！

# 概要

CLIで実行するNode.jsのアプリケーションを実行する時に、CLIからアプリケーションに引数を渡して、そしてその渡された引数をアプリケーションで解析する方法を紹介します。

## 必要な知識

この記事を理解するためには、Node.jsを使ってCLIでJavaScriptを実行する知識が必要です。

https://nodejs.dev/en/learn/run-nodejs-scripts-from-the-command-line

## 参考になった記事

https://2ality.com/2022/08/node-util-parseargs.html#more-ways-of-using-long-and-short-options

# `process.argv`を使い、Node.jsアプリケーションでCLI引数を読む

Node.jsのアプリケーションには、`process`のグローバル変数があり、この変数には、環境変数を始めとして、さまざまな便利な情報にアクセスすることができます。

今回、CLIの引数にアクセスするためには、`process.argv`を使います。

以下のようなJavaScriptを書いて、

```javascript:index.js
#!/usr/bin/env node
console.log(process.argv);
```

そしてNode.jsで実行してみますと、

```
node index.js --user churataro
```

以下の配列が出力されます。

```javascript
[
  '/usr/local/bin/node',
  '/Users/austin.mayer/Documents/practice/apply-saml-creds/test.js',
  '--user',
  'churataro'
]
```

文字列の配列なのです。そして、指定もしていない謎の文字列がIndex 0とIndex 1にきているのではないでしょうか。

この二つの文字列は何なのでしょうか？

### Index 0の`'/usr/local/bin/node'`

これは、この`index.js`を実行するのに使ったNode.jsのバイナリーのローカルパスです。つまり、ローカルにインストールしてあるNode.jsがどこにインストールされているかを教えてくれています。

### Index 1の`'/Users/austin.mayer/Documents/practice/apply-saml-creds/index.js'`

これは、`index.js`のローカルパスです。つまり、`index.js`を実行している時のNode.jsのPATHになります。

### Index 2以降

Index 2以降の値は、CLIで渡された引数になります。空白で分けてあります。

この部分の引数を解析したいので、通常、以下のようにIndex 2までの引数を無視して使います。

```javascript:index.js
#!/usr/bin/env node
const args = process.argv.slice(2);
console.log(args);
```

# キーと値でCLI引数を解析するロジックを作成する

次、CLI引数をキーと値にまとめてアプリケーションで使えるようにするためのロジックを書いていきましょう。

ここからはTypeScriptで書きますので、`tsc`を実行して、コンパイルされたものを実行しましょう。

今回のアプリケーションでは`--your-flag FLAG1`という形式で引数をCLIで指定する前提でロジックを作成します。

以下の関数をご覧ください。

```typescript：index.ts
const convertArgumentsToMap = (providedArguments: string[]) => {
  const argumentsMap = new Map<string, string | boolean>();
  const keyRegex = /--[a-z]{1,}-?[a-z]{1,}/;
  for (let index = 0; index < providedArguments.length; index += 2) {
    const key = providedArguments[index];
    if (!key) break;
    if (!keyRegex.test(key)) throw Error(`Invalid key provided: "${key}". Keys must be provided with two hyphens.`);
    const value = providedArguments[index + 1];
    argumentsMap.set(key, value ?? true);
  }

  return argumentsMap;
};
```

まず、`const argumentsMap = new Map<string, string | boolean>();`で引数のキーと値を保管する`Map`を作ります。

それから、`--flag-name FLAG-VALUE`という形式を取っているので、キーの`--flag-name`のように正しい形式で指定してもらっているかどうかを打診するために、正規表現の`/--[a-z]{1,}-?[a-z]{1,}/`を使います。

ちなみに、`--flag`のように、2番目のハイフンは任意なので、正規表現で`?`を使っています。本来だったら、回帰的な処理ができるような書き方をしたいところですが、JavaScriptではその正規表現ができないらしいです。

次に、`for`ループという、パッとしない手段で、一回の処理で配列の二つの値を引っ張り出しています。

一つ目の値はキーになるはずなので、それに対しては正規表現のテストを実行して正しい形式になっているかどうかを確かめます。

その上、配列の次の値も抽出して、その値があるかどうかをチェックした上で、`argumentsMap`に入れておきます。値がなかったら、`boolean`系のFlagだということで`true`を入れます。

CLI引数配列の最後までくると、`argumentMap`を返して終わり！

## 試しに引数を渡してみる

ここで、上記書いた関数を使って、CLI引数を解析してみます。

```javascript:index.js
#!/usr/bin/env node

const convertArgumentsToMap = (providedArguments) => {
  const argumentsMap = new Map();
  const keyRegex = /--[a-z]{1,}-?[a-z]{1,}/;
  for (let index = 0; index < providedArguments.length; index += 2) {
    const key = providedArguments[index];
    if (!key) break;
    if (!keyRegex.test(key)) throw Error(`Invalid key provided: "${key}". Keys must be provided with two hyphens.`);
    const value = providedArguments[index + 1];
    argumentsMap.set(key, value ?? true);
  }

  return argumentsMap;
};

const args = process.argv.slice(2);
const argsMap = convertArgumentsToMap(args);

console.log(argsMap);
```

上記のスクリプトを以下のCLI引数で実行してみます。

```
node test.js --user churataro --password churadata
```

すると、`Map`が以下のように出力されます。

```javascript
Map(2) { '--user' => 'churataro', '--password' => 'churadata' }
```

試しに、正しくない形式で引数を渡して実行してみます。

```
node test.js　user churataro --password churadata
```

すると以下のようにエラーが表示されます。

```
Error: Invalid key provided: "user". Keys must be provided with two hyphens.
```

# Node.jsの`node:utils.parseArgs`を使う

_**注意**　こちらは、試験的な機能で、最新のNode.js 18.8でしか使えない！_

最新のNode.jsには、もっと合理的な引数を解析してくれるツールが入っています。それは`node:utils.parseArgs`です。

以下、`parseArgs`の使い方を説明してまいります。

## `parseArgs`の引数について

`parseArgs`を実行する時に、引数にオブジェクトを渡します。

その中の必須な設定を説明します。

### `options`

こちらは、CLI引数でどのような引数を受け入れて使うかを設定するものです。

こちらもオブジェクトになりますが、以下のような型になっています。

```typescript
type Option = {
  type: "boolean" | "string"; // 必須
  short?: string; // 任意、省略フラグを定義するのに使う
  multiple?: boolean; // 任意、複数回指定していいかどうか
};
```

この`type Option`からなるオブジェクトなので、以下のような書き方ができます。

```typescript
const options: Record<string, Option> = {
  "account-number": {
    type: "string",
    short: "a",
    multiple: false,
  },
  role: {
    type: "string",
    short: "r",
    multiple: false,
  },
};
```

こうして指定した`options`は後ほど`parseArgs`で使います。

### `args`

こちらは、`process.argv`から取るCLI引数です。

```javascript
const args = process.argv.slice(2);
```

## 使ってみる

上記の情報を使って実行してみます。

```javascript:index.js
#!/usr/bin/env node
const { parseArgs } = require("node:util");

const options = {
  "account-number": {
    type: "string",
    short: "a",
    multiple: false,
  },
  role: {
    type: "string",
    short: "r",
    multiple: false,
  },
};

const args = process.argv.slice(2);

const parsedArgs = parseArgs({ options, args });

console.log(parsedArgs);
```

以下のようなコマンドで実行してみましょう。

```
node test.js --account-number 100 --role Admin
```

結果：

```javascript
{
  values: [Object: null prototype] { 'account-number': '100', role: 'Admin' },
  positionals: []
}
```

ちゃんと、`account-number`と`role`が取れていますね！
*_`positionals`については今回の記事で説明しません。気になる方は<a href="https://nodejs.org/api/util.html#utilparseargsconfig">公式ドキュメント</a>をご参照ください。_


上記の`options`で任意の`short`も設定しているのですが、`short`を設定するとCLIでは以下のような引数の渡し方ができます。

```
node test.js -a 100 -r Admin
```

また、設定されていない引数を渡すと、以下のようなエラーが出ます。

```
node test.js --account-number 100 --role Admin --unknown
```

結果：

```
TypeError [ERR_PARSE_ARGS_UNKNOWN_OPTION]: Unknown option '--unknown'
    at new NodeError (node:internal/errors:393:5)
    at checkOptionUsage (node:internal/util/parse_args/parse_args:97:11)
    at node:internal/util/parse_args/parse_args:327:9
    at Array.forEach (<anonymous>)
    at parseArgs (node:internal/util/parse_args/parse_args:324:3)
    at Object.<anonymous> (/Users/austin.mayer/Documents/practice/apply-saml-creds/test.js:19:20)
    at Module._compile (node:internal/modules/cjs/loader:1119:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1173:10)
    at Module.load (node:internal/modules/cjs/loader:997:32)
    at Module._load (node:internal/modules/cjs/loader:838:12) {
  code: 'ERR_PARSE_ARGS_UNKNOWN_OPTION'
}
```

自作のコードより便利なので、使いたい！と思いますが、まだ試験的な段階なので、完全に推奨できません。今後の楽しみにしておきましょう！

# まとめ

以上、Node.jsでCLI引数を解析する方法を紹介してまいりましたが、いかがでしょうか？

最後に紹介した`parseArgs`が待ち遠しいですね。

今回、CLI引数を解析する方法について調べるきっかけは、筆者が初めてnpmに公開したパッケージで使うからです。

そのパッケージは、AWSにSAMLでログインした場合、そのSAML Responseを使って、ローカルの`.aws/credentials`ファイルにも反映させるスクリプトです。

https://www.npmjs.com/package/apply-aws-credentials-from-samlresponse

筆者は、毎回手書きでやるのがしににりーだったので、<q>Nodeでやったら、面白いはず！</q>と思い、またさらに、<q>npmに公開する方法もついでに勉強したらいいはず！</q>とも思い、随分楽しい経験になりました。

実際、誰1人として使うとは到底思いませんが！（笑）