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

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

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

ECMAScriptモジュールを使う

今回のユースケースでは、node:utilモジュールを利用するにあたって、基本文法で学んだECMAScriptモジュールを使います。 node:utilモジュールは、次のようにimport文を使ってインポートできます。

// `node:util`モジュールを、utilオブジェクトとしてインポートする
import * as util from "node:util";

ただし、ECMAScriptモジュールを扱う場合には、Node.jsに対してJavaScriptファイルがどのモジュール形式であるかを明示する必要があります。 なぜなら、Node.jsCommonJSモジュールという別のモジュール形式もサポートしており、CommonJSモジュール形式ではimport文は利用できないためです。

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

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

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

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

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": {}
}

[コラム] CommonJSモジュール

CommonJSモジュールとは、Node.js環境で利用されているJavaScriptのモジュール化の仕組みです。 CommonJSモジュールはECMAScriptモジュールの仕様が策定されるより前からNode.jsで使われています。

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

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アプリケーションを開発する際には、相互運用性に注意しておく必要があるでしょう。

コマンドライン引数からファイルパスを取得する

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

$ node main.js ./sample.md

コマンドライン引数をパースするためには、node:utilモジュールのparseArgs関数を利用します。

// `node:util`モジュールを、utilオブジェクトとしてインポートする
import * as util from "node:util";

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

parseArgs関数は、コマンドライン引数をパースした結果としてvaluespositionalsの2つのプロパティを持つオブジェクトを返します。 valuesオブジェクトには、--key=valueのようなオプションや--flagのようなフラグをパースした結果が保存されています。 positionals配列には、オプションやフラグ以外の引数が配列として順番に格納されています。 デフォルトでは、positionals配列はパース結果には含まれないため、allowPositionalsオプションをtrueにすることで含まれるようになります。

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

main.js

import * as util from "node:util";

// コマンドライン引数をparseArgs関数でパースする
const {
    positionals
} = util.parseArgs({
    // オプションやフラグ以外の引数を渡すことを許可する
    allowPositionals: true
});
// ファイルパスをpositionals配列から取り出す
const filePath = positionals[0];
console.log(filePath);

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

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

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

[エラー例] SyntaxError: Cannot use import statement outside a module

import文をECMAScriptモジュールの外で使うことはできません」というエラーが出ています。main.js の実行でこのエラーが出る場合は、Node.jsがmain.jsファイルをECMAScriptモジュールだと判別できていないことを意味します。

import * as util from "node:util";
^^^^^^

SyntaxError: Cannot use import statement outside a module

ECMAScriptモジュールを使うで述べたように、package.jsontypeフィールドをmoduleに設定しましょう。

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

  • process.argv配列にnodeコマンドのコマンドライン引数が格納されていることを確認した
  • ECMAScriptモジュールを使ってパッケージを読み込めることを確認した
  • node:utilモジュールのparseArgs関数を使ってコマンドライン引数をパースできることを確認した
  • コマンドライン引数で渡されたファイルパスを取得してコンソールに出力できた
1. package.json and file extensions
2. Interoperability with CommonJS