コマンドライン引数を処理する

このユースケースで作成する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はnpmnpm 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 [email protected]

インストールが完了すると、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.jsCommonJSモジュールという別のモジュール形式もサポートしており、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.jsontype フィールドを追加します。

# 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.mdprocess.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.jsontypeフィールドをmoduleに設定しましょう。

このセクションのチェックリスト

  • process.argv配列にnodeコマンドのコマンドライン引数が格納されていることを確認した
  • npmを使ってパッケージをインストールする方法を理解した
  • ECMAScriptモジュールを使ってパッケージを読み込めることを確認した
  • commanderを使ってコマンドライン引数をパースできることを確認した
  • コマンドライン引数で渡されたファイルパスを取得してコンソールに出力できた
1. --saveオプションをつけてインストールしたのと同じ意味。npm 5.0.0からは--saveがデフォルトオプションとなりました。
2. package.json and file extensions
3. Interoperability with CommonJS