Skip to content

Commit

Permalink
fix(nodecli): commanderパッケージ を node:utilparseArg に変更 (#1757)
Browse files Browse the repository at this point in the history
* fix(nodecli): commanderをコードから削除

* refactor: commanderのインストールを削除、セットアップセクションを追加

* fix: commanderをparseArgに置き換える

* fix

* fix

* fix: main-3.jsは利用しな苦なった

* fix

* fix

* fix

* fix order

* fix: node:fsに統一

* `node:`をコラムに移動

* fix

* fix

* fix

* fix

* fix

* remove empty line

* fix

* fix

* npm link

* link

* fix

* fix

* fix

* シンプルに

* 標準モジュールはインストールが不要なことを明記

* fix
  • Loading branch information
azu authored Aug 3, 2024
1 parent 631fc46 commit 2a0712b
Show file tree
Hide file tree
Showing 25 changed files with 288 additions and 303 deletions.
119 changes: 47 additions & 72 deletions source/use-case/nodecli/argument-parse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,78 +44,42 @@ $ node main.js one two=three four

## コマンドライン引数をパースする {#parse-args}

`process.argv`配列を使えばコマンドライン引数を取得できますが、取得できる情報にはアプリケーションに不要なものも含まれています
`process.argv`配列を使えばコマンドライン引数を取得できますが、アプリケーションには不要なものも含まれています
また、文字列の配列として渡されるため、フラグのオンオフのような真偽値を受け取るときにも不便です。
そのため、アプリケーションでコマンドライン引数を扱うときには、一度パースして扱いやすい値に整形するのが一般的です。

今回は[commander][]というライブラリを使ってコマンドライン引数をパースしてみましょう。
文字列処理を自前で行うこともできますが、このような一般的な処理は既存のライブラリを使うと簡単に書けます。

### `commander`パッケージをインストールする {#install-commander}

commanderは[npm][]`npm install`コマンドを使ってインストールできます。
まだnpmの実行環境を用意できていなければ、先に「[アプリケーション開発の準備][]」の章を参照してください。

npmでパッケージをインストールする前に、まずは`package.json`というファイルを作成します。
`package.json`とは、アプリケーションが依存するパッケージの種類やバージョンなどの情報を記録するJSON形式のファイルです。
`package.json`ファイルのひな形は、`npm init`コマンドで生成できます。
通常は対話式のプロンプトによって情報を設定しますが、ここではすべてデフォルト値で`package.json`を作成する`--yes`オプションを付与します。

`nodecli`のディレクトリ内で、`npm init --yes`コマンドを実行して`package.json`を作成しましょう。

```shell
$ npm init --yes
```

生成された`package.json`ファイルは次のようになっています。

[import, title:"package.json"](src/package.init.json)

`package.json`ファイルが用意できたら、`npm install`コマンドを使って`commander`パッケージをインストールします。
このコマンドの引数にはインストールするパッケージの名前とそのバージョンを`@`記号でつなげて指定できます。
バージョンを指定せずにインストールすれば、その時点での最新の安定版が自動的に選択されます。
次のコマンドを実行して、commanderのバージョン9.0をインストールします。[^1]

```shell
$ npm install commander@9.0
```

インストールが完了すると、`package.json`ファイルは次のようになっています。

[import, title:"package.json"](src/package.install.json)

また、`npm install`をすると同時に`package-lock.json`ファイルが生成されています。
このファイルはnpmがインストールしたパッケージの、実際のバージョンを記録するためのものです。
先ほどcommanderのバージョンを`9.0`としましたが、実際にインストールされるのは`9.0.x`に一致する最新のバージョンです。
`package-lock.json`ファイルには実際にインストールされたバージョンが記録されています。
これによって、再び`npm install`を実行したときに、異なるバージョンがインストールされるのを防ぎます。

今回は、Node.jsの標準モジュールである[`node:util`モジュール][][parseArgs][]という関数を使ってコマンドライン引数をパースしてみましょう。
コマンドライン引数のパース処理を自前で行うこともできますが、このような一般的な処理はNode.jsの標準モジュールやサードパーティ製のライブラリを使うことで簡単に実装できます。
今回利用する`node:util`モジュールは、Node.js自体に同梱されている標準モジュールであるため、npmを使って別途インストールする必要はありません。

### ECMAScriptモジュールを使う {#esmodule}

今回のユースケースでは、インストールした`commander`パッケージを利用するにあたって、基本文法で学んだ[ECMAScriptモジュール][]を使います。
`commander`パッケージはECMAScriptモジュールに対応しているため、次のように`import`文を使って変数や関数などをインポートできます
今回のユースケースでは、`node:util`モジュールを利用するにあたって、基本文法で学んだ[ECMAScriptモジュール][]を使います。
`node:util`モジュールは、次のように`import`文を使ってインポートできます

<!-- doctest:disable -->
```js
import { program } from "commander";
// `node:util`モジュールを、utilオブジェクトとしてインポートする
import * as util from "node:util";
```

ただし、ECMAScriptモジュールのパッケージをインポートするには、インポート元のファイルもECMAScriptモジュールでなければなりません
ただし、ECMAScriptモジュールを扱う場合には、Node.jsに対してJavaScriptファイルがどのモジュール形式であるかを明示する必要があります
なぜなら、[Node.js][][CommonJSモジュール][]という別のモジュール形式もサポートしており、CommonJSモジュール形式では`import`文は利用できないためです。
そのため、これから実行するJavaScriptファイルがどちらの形式であるかをNode.jsに教える必要があります。

Node.jsはもっとも近い上位ディレクトリの `package.json` が持つ `type` フィールドの値によってJavaScriptファイルのモジュール形式を判別します。
`type`フィールドが `module` であればECMAScriptモジュールとして、`type`フィールドが `commonjs` であればCommonJSモジュールとして扱われます。[^2]
`type`フィールドが `module` であればECMAScriptモジュールとして、`type`フィールドが `commonjs` であればCommonJSモジュールとして扱われます。[^1]
また、JavaScriptファイルの拡張子によって明示的に示すこともできます。拡張子が `.mjs` である場合はECMAScriptモジュールとして、`.cjs` である場合はCommonJSモジュールであると判別されます。

今回は `main.js` を ECMAScriptモジュールとして判別させるために、次のように `package.json``type` フィールドを追加します。
今回は `main.js` を ECMAScriptモジュールとして判別させるために、次のコマンドで `package.json``type` フィールドを追加します。
まだ、`package.json`を作成していない場合は、先に「[Node.jsプロジェクトのセットアップ][]」を参照してください。

```shell
# npm pkg コマンドで type フィールドの値をセットする
# npm pkg コマンドで、package.jsonの type フィールドの値をセットする
$ npm pkg set type=module
```

コマンドが実行できたら`package.json`ファイルに `type` フィールドが追加されていることを確認してください。

[import, title:"package.json"](src/package.json)

#### [コラム] CommonJSモジュール {#commonjs-module}
Expand All @@ -125,7 +89,7 @@ CommonJSモジュールは[ECMAScriptモジュール][]の仕様が策定され

現在はNode.jsでもECMAScriptモジュールがサポートされていますが、`fs` などの標準モジュールはCommonJSモジュールとして提供されています。
また、サードパーティ製のライブラリや長く開発が続けられているプロジェクトのソースコードなどでも、CommonJSモジュールを利用する場面は少なくありません。
そのため、この2つのモジュール形式が共存する場合には、開発者はモジュール形式間の相互運用性(互いを組み合わせた時の動作)に注意する必要があります。[^3]
そのため、この2つのモジュール形式が共存する場合には、開発者はモジュール形式間の相互運用性(互いを組み合わせた時の動作)に注意する必要があります。[^2]

Node.jsはECMAScriptモジュールからCommonJSモジュールをインポートする方向の相互運用性をサポートしています。
たとえば、次のようにCommonJSモジュールで`exports`オブジェクトを使ってエクスポートされたオブジェクトは、ECMAScriptモジュールで`import`文を使ってインポートできます。
Expand All @@ -146,40 +110,51 @@ import { key } from "./lib.cjs";

### コマンドライン引数からファイルパスを取得する {#get-file-path}

先ほどインストールした`commander`パッケージを使って、コマンドライン引数として渡されたファイルパスを取得しましょう。
`node:util`モジュールを使って、コマンドライン引数として渡されたファイルパスを取得しましょう。
このCLIアプリケーションでは、処理の対象とするファイルパスを次のようなコマンドの形式で受け取ります。

```shell
$ node main.js ./sample.md
```

commanderでコマンドライン引数をパースするためには、インポートした`program`オブジェクトの`parse`メソッドにコマンドライン引数を渡します
コマンドライン引数をパースするためには、`node:util`モジュールの`parseArgs`関数を利用します

<!-- doctest:disable -->
```js
// commanderモジュールからprogramオブジェクトをインポートする
import { program } from "commander";
// コマンドライン引数をcommanderでパースする
program.parse(process.argv);
// `node:util`モジュールを、utilオブジェクトとしてインポートする
import * as util from "node:util";

// コマンドライン引数をparseArgs関数でパースする
const {
values,
positionals
} = util.parseArgs({
// オプションやフラグ以外の引数を渡すことを許可する
allowPositionals: true
});
console.log(values); // オプションやフラグを含むオブジェクト
console.log(positionals); // フラグ以外の引数の配列
```

`parse`メソッドを呼び出すと、コマンドライン引数をパースした結果を`program`オブジェクトから取り出せるようになります。
今回の例では、ファイルパスは`program.args`配列に格納されています。
`program.args`配列には`--key=value`のようなオプションや`--flag`のようなフラグを取り除いた残りのコマンドライン引数が順番に格納されています。
`parseArgs`関数は、コマンドライン引数をパースした結果として`values``positionals`の2つのプロパティを持つオブジェクトを返します。
`values`オブジェクトには、`--key=value`のようなオプションや`--flag`のようなフラグをパースした結果が保存されています。
`positionals`配列には、オプションやフラグ以外の引数が配列として順番に格納されています。
デフォルトでは、`positionals`配列はパース結果には含まれないため、`allowPositionals`オプションを`true`にすることで含まれるようになります。

今回の`main.js`に渡す`./sample.md`引数はオプションやフラグではないので、`positionals`配列に格納されます。
それでは`main.js`を次のように変更し、コマンドライン引数で渡されたファイルパスを取得しましょう。

[import title:"main.js"](src/main-2.js)

次のコマンドを実行すると、`program.args`配列に格納された`./sample.md`文字列が取得されてコンソールに出力されます。
`./sample.md``process.argv`配列では3番目に存在していましたが、パース後の`program.args`配列では1番目になって扱いやすくなっています。
次のコマンドを実行すると、`positionals`配列の先頭に格納された`./sample.md`文字列が取得されてコンソールに出力されます。
`./sample.md``process.argv`配列では3番目に存在していましたが、パース後の`positionals`配列では1番目になって扱いやすくなっています。

```shell
$ node main.js ./sample.md
./sample.md
```

このように、`process.argv`配列を直接扱うよりも、commanderのようなライブラリを使うことで宣言的にコマンドライン引数を定義して処理できます
このように、`process.argv`配列を直接扱うよりも、`node:util``parseArgs`関数を利用すると、コマンドライン引数をより扱いやすい形にパースできます
次のセクションではコマンドライン引数から取得したファイルパスを元に、ファイルを読み込む処理を追加していきます。

#### [エラー例] SyntaxError: Cannot use import statement outside a module {#syntax-error-import-statement}
Expand All @@ -188,7 +163,7 @@ $ node main.js ./sample.md

<!-- doctest:disable -->
```shell
import { program } from "commander";
import * as util from "node:util";
^^^^^^

SyntaxError: Cannot use import statement outside a module
Expand All @@ -199,18 +174,18 @@ SyntaxError: Cannot use import statement outside a module
## このセクションのチェックリスト {#section-checklist}

- `process.argv`配列に`node`コマンドのコマンドライン引数が格納されていることを確認した
- npmを使ってパッケージをインストールする方法を理解した
- ECMAScriptモジュールを使ってパッケージを読み込めることを確認した
- commanderを使ってコマンドライン引数をパースできることを確認した
- `node:util`モジュールの`parseArgs`関数を使ってコマンドライン引数をパースできることを確認した
- コマンドライン引数で渡されたファイルパスを取得してコンソールに出力できた

[commander]: https://github.com/tj/commander.js/
[npm]: https://www.npmjs.com/

[npmのGitHubリポジトリ]: https://github.com/npm/npm
[CommonJSモジュール]: https://nodejs.org/docs/latest/api/modules.html
[Node.js]: https://nodejs.org/
[アプリケーション開発の準備]: ../../setup-local-env/README.md
[ECMAScriptモジュール]: ../../../basic/module/README.md
[^1]: --saveオプションをつけてインストールしたのと同じ意味。npm 5.0.0からは--saveがデフォルトオプションとなりました。
[^2]: [package.json and file extensions](https://nodejs.org/api/packages.html#packagejson-and-file-extensions)
[^3]: [Interoperability with CommonJS](https://nodejs.org/api/esm.html#interoperability-with-commonjs)
[`node:util`モジュール]: https://nodejs.org/api/util.html
[parseArgs]: https://nodejs.org/api/util.html#utilparseargsconfig
[Node.jsプロジェクトのセットアップ]: ../helloworld/README.md#setup-nodejs-project
[^1]: [package.json and file extensions](https://nodejs.org/api/packages.html#packagejson-and-file-extensions)
[^2]: [Interoperability with CommonJS](https://nodejs.org/api/esm.html#interoperability-with-commonjs)
17 changes: 10 additions & 7 deletions source/use-case/nodecli/argument-parse/src/main-2.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
// commanderモジュールからprogramオブジェクトをインポートする
import { program } from "commander";
import * as util from "node:util";

// コマンドライン引数をcommanderでパースする
program.parse(process.argv);

// ファイルパスをprogram.args配列から取り出す
const filePath = program.args[0];
// コマンドライン引数をparseArgs関数でパースする
const {
positionals
} = util.parseArgs({
// オプションやフラグ以外の引数を渡すことを許可する
allowPositionals: true
});
// ファイルパスをpositionals配列から取り出す
const filePath = positionals[0];
console.log(filePath);
13 changes: 1 addition & 12 deletions source/use-case/nodecli/argument-parse/src/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 0 additions & 15 deletions source/use-case/nodecli/argument-parse/src/package.install.json

This file was deleted.

4 changes: 1 addition & 3 deletions source/use-case/nodecli/argument-parse/src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,5 @@
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^9.0.0"
}
"dependencies": {}
}
Loading

0 comments on commit 2a0712b

Please sign in to comment.