コマンドライン引数を処理する
このユースケースで作成する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.jsはCommonJSモジュールという別のモジュール形式もサポートしており、CommonJSモジュール形式ではimport
文は利用できないためです。
Node.jsはもっとも近い上位ディレクトリの package.json
が持つ type
フィールドの値によってJavaScriptファイルのモジュール形式を判別します。
type
フィールドが module
であればECMAScriptモジュールとして、type
フィールドが commonjs
であればCommonJSモジュールとして扱われます。1
また、JavaScriptファイルの拡張子によって明示的に示すこともできます。拡張子が .mjs
である場合はECMAScriptモジュールとして、.cjs
である場合はCommonJSモジュールであると判別されます。
今回は main.js
を ECMAScriptモジュールとして判別させるために、次のコマンドで package.json
にtype
フィールドを追加します。
まだ、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
関数は、コマンドライン引数をパースした結果としてvalues
とpositionals
の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.md
はprocess.argv
配列では3番目に存在していましたが、パース後のpositionals
配列では1番目になって扱いやすくなっています。
$ node main.js ./sample.md
./sample.md
このように、process.argv
配列を直接扱うよりも、node:util
のparseArgs
関数を利用すると、コマンドライン引数をより扱いやすい形にパースできます。
次のセクションではコマンドライン引数から取得したファイルパスを元に、ファイルを読み込む処理を追加していきます。
[エラー例] 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.json
のtype
フィールドをmodule
に設定しましょう。
このセクションのチェックリスト
process.argv
配列にnode
コマンドのコマンドライン引数が格納されていることを確認した- ECMAScriptモジュールを使ってパッケージを読み込めることを確認した
node:util
モジュールのparseArgs
関数を使ってコマンドライン引数をパースできることを確認した- コマンドライン引数で渡されたファイルパスを取得してコンソールに出力できた
1. package.json and file extensions ↩
2. Interoperability with CommonJS ↩