コマンドライン引数を処理する
このユースケースで作成するCLIアプリケーションの目的は、コマンドライン引数として与えられたMarkdownファイルをHTMLへ変換することです。
このセクションではnode
コマンドでスクリプトを実行する際に引数を渡し、コマンドライン引数としてパースするところまでを行います。
process
オブジェクトとコマンドライン引数
コマンドライン引数を扱う前に、まずはprocess
オブジェクトについて触れておきます。
process
オブジェクトはNode.js実行環境のグローバル変数のひとつです。
process
オブジェクトが提供するのは、現在のNode.jsの実行プロセスについて、情報の取得と操作をするAPIです。
詳細は公式ドキュメントを参照してください。
コマンドライン引数へのアクセスを提供するのは、process
オブジェクトのargv
プロパティで、文字列の配列になっています。
次のようにmain.js
を変更し、process.argv
をコンソールに出力しましょう。
main.js
// コンソールにコマンドライン引数を出力する
console.log(process.argv);
このスクリプトを次のようにコマンドライン引数をつけて実行してみましょう。
$ node main.js one two=three four
このコマンドの実行結果は次のようになります。
[
'/usr/local/bin/node', // Node.jsの実行プロセスのパス
'/Users/laco/nodecli/main.js', // 実行したスクリプトファイルのパス
'one', // 1番目の引数
'two=three', // 2番目
'four' // 3番目
]
1番目と2番目の要素は常にnode
コマンドと実行されたスクリプトのファイルパスになります。
つまりアプリケーションがコマンドライン引数として使うのは、3番目以降の要素です。
コマンドライン引数をパースする
process.argv
配列を使えばコマンドライン引数を取得できますが、取得できる情報にはアプリケーションに不要なものも含まれています。
また、文字列の配列として渡されるため、フラグのオンオフのような真偽値を受け取るときにも不便です。
そのため、アプリケーションでコマンドライン引数を扱うときには、一度パースして扱いやすい値に整形するのが一般的です。
今回はcommanderというライブラリを使ってコマンドライン引数をパースしてみましょう。 文字列処理を自前で行うこともできますが、このような一般的な処理は既存のライブラリを使うと簡単に書けます。
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
を作成しましょう。
$ npm init --yes
生成されたpackage.json
ファイルは次のようになっています。
package.json
{
"name": "nodecli",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
package.json
ファイルが用意できたら、npm install
コマンドを使ってcommander
パッケージをインストールします。
このコマンドの引数にはインストールするパッケージの名前とそのバージョンを@
記号でつなげて指定できます。
バージョンを指定せずにインストールすれば、その時点での最新の安定版が自動的に選択されます。
次のコマンドを実行して、commanderのバージョン9.0をインストールします。1
$ npm install commander@9.0
インストールが完了すると、package.json
ファイルは次のようになっています。
package.json
{
"name": "nodecli",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^9.0.0"
}
}
また、npm install
をすると同時にpackage-lock.json
ファイルが生成されています。
このファイルはnpmがインストールしたパッケージの、実際のバージョンを記録するためのものです。
先ほどcommanderのバージョンを9.0
としましたが、実際にインストールされるのは9.0.x
に一致する最新のバージョンです。
package-lock.json
ファイルには実際にインストールされたバージョンが記録されています。
これによって、再びnpm install
を実行したときに、異なるバージョンがインストールされるのを防ぎます。
ECMAScriptモジュールを使う
今回のユースケースでは、インストールしたcommander
パッケージを利用するにあたって、基本文法で学んだECMAScriptモジュールを使います。
commander
パッケージはECMAScriptモジュールに対応しているため、次のようにimport
文を使って変数や関数などをインポートできます。
import { program } from "commander";
ただし、ECMAScriptモジュールのパッケージをインポートするには、インポート元のファイルもECMAScriptモジュールでなければなりません。
なぜなら、Node.jsはCommonJSモジュールという別のモジュール形式もサポートしており、CommonJSモジュール形式ではimport
文は利用できないためです。
そのため、これから実行するJavaScriptファイルがどちらの形式であるかをNode.jsに教える必要があります。
Node.jsはもっとも近い上位ディレクトリの package.json
が持つ type
フィールドの値によってJavaScriptファイルのモジュール形式を判別します。
type
フィールドが module
であればECMAScriptモジュールとして、type
フィールドが commonjs
であればCommonJSモジュールとして扱われます。2
また、JavaScriptファイルの拡張子によって明示的に示すこともできます。拡張子が .mjs
である場合はECMAScriptモジュールとして、.cjs
である場合はCommonJSモジュールであると判別されます。
今回は main.js
を ECMAScriptモジュールとして判別させるために、次のように package.json
にtype
フィールドを追加します。
# npm pkg コマンドで type フィールドの値をセットする
$ npm pkg set type=module
package.json
{
"name": "nodecli",
"version": "1.0.0",
"description": "",
"main": "main.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"commander": "^9.0.0"
}
}
[コラム] CommonJSモジュール
CommonJSモジュールとは、Node.js環境で利用されているJavaScriptのモジュール化の仕組みです。 CommonJSモジュールはECMAScriptモジュールの仕様が策定されるより前からNode.jsで使われています。
現在はNode.jsでもECMAScriptモジュールがサポートされていますが、fs
などの標準モジュールはCommonJSモジュールとして提供されています。
また、サードパーティ製のライブラリや長く開発が続けられているプロジェクトのソースコードなどでも、CommonJSモジュールを利用する場面は少なくありません。
そのため、この2つのモジュール形式が共存する場合には、開発者はモジュール形式間の相互運用性(互いを組み合わせた時の動作)に注意する必要があります。3
Node.jsはECMAScriptモジュールからCommonJSモジュールをインポートする方向の相互運用性をサポートしています。
たとえば、次のようにCommonJSモジュールでexports
オブジェクトを使ってエクスポートされたオブジェクトは、ECMAScriptモジュールでimport
文を使ってインポートできます。
Node.jsの標準モジュールはECMAScriptモジュールのJavaScriptファイルからでも利用できますが、それはこの相互運用性によるものです。
// lib.cjs
exports.key = "value";
// app.mjs
import { key } from "./lib.cjs";
一方で、CommonJSモジュールからECMAScriptモジュールをインポートする方向の相互運用性はサポートされていません。 もし既存のライブラリから提供されるモジュールがECMAScriptモジュールであれば、それを使うアプリケーションもECMAScriptモジュールで書かれている必要があります。 複数のパッケージを利用しながらNode.jsアプリケーションを開発する際には、相互運用性に注意しておく必要があるでしょう。
コマンドライン引数からファイルパスを取得する
先ほどインストールしたcommander
パッケージを使って、コマンドライン引数として渡されたファイルパスを取得しましょう。
このCLIアプリケーションでは、処理の対象とするファイルパスを次のようなコマンドの形式で受け取ります。
$ node main.js ./sample.md
commanderでコマンドライン引数をパースするためには、インポートしたprogram
オブジェクトのparse
メソッドにコマンドライン引数を渡します。
// commanderモジュールからprogramオブジェクトをインポートする
import { program } from "commander";
// コマンドライン引数をcommanderでパースする
program.parse(process.argv);
parse
メソッドを呼び出すと、コマンドライン引数をパースした結果をprogram
オブジェクトから取り出せるようになります。
今回の例では、ファイルパスはprogram.args
配列に格納されています。
program.args
配列には--key=value
のようなオプションや--flag
のようなフラグを取り除いた残りのコマンドライン引数が順番に格納されています。
それではmain.js
を次のように変更し、コマンドライン引数で渡されたファイルパスを取得しましょう。
main.js
// commanderモジュールからprogramオブジェクトをインポートする
import { program } from "commander";
// コマンドライン引数をcommanderでパースする
program.parse(process.argv);
// ファイルパスをprogram.args配列から取り出す
const filePath = program.args[0];
console.log(filePath);
次のコマンドを実行すると、program.args
配列に格納された./sample.md
文字列が取得されてコンソールに出力されます。
./sample.md
はprocess.argv
配列では3番目に存在していましたが、パース後のprogram.args
配列では1番目になって扱いやすくなっています。
$ node main.js ./sample.md
./sample.md
このように、process.argv
配列を直接扱うよりも、commanderのようなライブラリを使うことで宣言的にコマンドライン引数を定義して処理できます。
次のセクションではコマンドライン引数から取得したファイルパスを元に、ファイルを読み込む処理を追加していきます。
[エラー例] SyntaxError: Cannot use import statement outside a module
「import
文をECMAScriptモジュールの外で使うことはできません」というエラーが出ています。main.js
の実行でこのエラーが出る場合は、Node.jsがmain.js
ファイルをECMAScriptモジュールだと判別できていないことを意味します。
import { program } from "commander";
^^^^^^
SyntaxError: Cannot use import statement outside a module
ECMAScriptモジュールを使うで述べたように、package.json
のtype
フィールドをmodule
に設定しましょう。
このセクションのチェックリスト
process.argv
配列にnode
コマンドのコマンドライン引数が格納されていることを確認した- npmを使ってパッケージをインストールする方法を理解した
- ECMAScriptモジュールを使ってパッケージを読み込めることを確認した
- commanderを使ってコマンドライン引数をパースできることを確認した
- コマンドライン引数で渡されたファイルパスを取得してコンソールに出力できた
1. --saveオプションをつけてインストールしたのと同じ意味。npm 5.0.0からは--saveがデフォルトオプションとなりました。 ↩
2. package.json and file extensions ↩
3. Interoperability with CommonJS ↩