ファイルを読み込む

前のセクションではコマンドライン引数からファイルパスを取得して利用できるようになりました。 このセクションでは渡されたファイルパスを元にMarkdownファイルを読み込んで、標準出力に表示してみましょう。

node:fsモジュールを使ってファイルを読み込む

前のセクションで取得できるようになったファイルパスを元に、ファイルを読み込みましょう。 Node.jsでファイルの読み書きを行うには、標準モジュールのnode:fsモジュールを使います。 まずは読み込む対象のファイルを作成しましょう。sample.mdという名前でmain.jsと同じnodecliディレクトリに配置します。

sample.md

# sample

node:fsモジュール

node:fsモジュールは、Node.jsでファイルの読み書きを行うための基本的な関数を提供するモジュールです。

node:fsモジュールは同期APIと非同期APIの両方が提供されています。 一方で、node:fs/promisesモジュールには非同期形式のAPIのみが提供されています。 この書籍では分かりやすさのために、非同期形式のみのAPIを提供するnode:fs/promisesモジュールを利用します。

次のコードは、ECMAScriptモジュールのimport * as構文を使って、node:fs/promises モジュール全体をfsオブジェクトとしてインポートしています。

// fs/promisesモジュール全体を読み込む
import * as fs from "node:fs/promises";

もちろん、次のように名前付きインポートを使って、node:fs/promisesモジュール全体ではなく一部のAPIだけを利用することもできます。

// fs/promisesモジュールからreadFile関数を読み込む
import { readFile } from "node:fs/promises";

node:fs/promisesの非同期APIは、モジュール名からもわかるようにPromiseを返します。 ファイルの読み書きといった非同期処理が成功したときには、返されたPromiseインスタンスがresolveされます。 一方、ファイルの読み書きといった非同期処理が失敗したときには、返されたPromiseインスタンスがrejectされます。

次のサンプルコードは、指定したファイルを読み込むnode:fs/promisesreadFileメソッドの例です。

// 非同期APIを提供するfs/promisesモジュールを読み込む
import * as fs from "node:fs/promises";

fs.readFile("sample.md").then(file => {
    console.log(file);
}).catch(err => {
    console.error(err);
});

そして、次のサンプルコードは、同じく指定したファイルを読み込むnode:fsモジュールのreadFileSyncメソッドの例です。 Node.jsでは非同期APIと同期APIがどちらもあるAPIには、分かりやすくSyncがメソッド名の末尾に含まれています。

// 同期APIを提供するfsモジュールを読み込む
import * as fs from "node:fs";

try {
    const file = fs.readFileSync("sample.md");
} catch (err) {
    // ファイルが読み込めないなどのエラーが発生したときに呼ばれる
}

Node.jsはシングルスレッドなので、他の処理をブロックしにくい非同期形式のAPIを選ぶことがほとんどです。 Node.jsにはnode:fs/promisesモジュール以外にも多くの非同期APIがあるので、非同期処理に慣れておきましょう。

readFile関数を使う

それではnode:fs/promisesモジュールのreadFileメソッドを使ってsample.mdファイルを読み込んでみましょう。 次のようにmain.jsを変更し、コマンドライン引数から取得したファイルパスを元にファイルを読み込んでコンソールに出力します。

main.js

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

// コマンドライン引数からファイルパスを取得する
const { positionals } = util.parseArgs({
    allowPositionals: true
});
const filePath = positionals[0];
// ファイルを非同期で読み込む
fs.readFile(filePath).then(file => {
    console.log(file);
});

sample.mdを引数に渡した実行結果は次のようになります。 文字列になっていないのは、fs.readFile関数でファイルをオプションなしで読み込んだ結果は、ファイルの中身を表すBufferインスタンスとなるためです。 Bufferインスタンスはファイルの中身をバイト列として保持しています。 そのため、そのままconsole.logメソッドに渡しても人間が読める文字列にはなりません。

$ node main.js sample.md
<Buffer 23 20 73 61 6d 70 6c 65>

fs.readFile関数は引数によってファイルの読み込み方を指定できます。 ファイルのエンコードを第二引数であらかじめ指定しておけば、自動的に文字列に変換された状態でコールバック関数に渡されます。 次のようにmain.jsを変更し、読み込まれるファイルをUTF-8として変換させます。

main.js

import * as util from "node:util";
import * as fs from "node:fs/promises";

const { positionals } = util.parseArgs({
    allowPositionals: true
});
const filePath = positionals[0];
// ファイルをUTF-8として非同期で読み込む
fs.readFile(filePath, { encoding: "utf8" }).then(file => {
    console.log(file);
});

先ほどと同じコマンドをもう一度実行すると、実行結果は次のようになります。 sample.mdファイルの中身を文字列として出力できました。

$ node main.js sample.md
# sample

エラーハンドリング

ファイルの読み書きは存在の有無や権限、ファイルシステムの違いなどによって例外が発生しやすいので、必ずエラーハンドリング処理を書きましょう。

次のようにmain.jsを変更し、readFileの返り値であるPromiseオブジェクトに対してcatchメソッドを追加するだけのシンプルなエラーハンドリングです。 エラーが発生していたときにはエラーメッセージを表示し、process.exit関数に終了ステータスを指定してプロセスを終了しています。 ここでは、一般的なエラーを表す終了ステータスの1でプロセスを終了しています。

main.js

import * as util from "node:util";
import * as fs from "node:fs/promises";

const { positionals } = util.parseArgs({
    allowPositionals: true
});
const filePath = positionals[0];
// ファイルを非同期で読み込む
fs.readFile(filePath, { encoding: "utf8" }).then(file => {
    console.log(file);
}).catch(err => {
    console.error(err.message);
    // 終了ステータス 1(一般的なエラー)としてプロセスを終了する
    process.exit(1);
});

存在しないファイルであるnotfound.mdをコマンドライン引数に渡して実行すると、次のようにエラーが発生して終了します。

$ node main.js notfound.md
ENOENT: no such file or directory, open 'notfound.md'

これでコマンドライン引数に指定したファイルを読み込んで標準出力に表示できました。 次のセクションでは読み込んだMarkdownファイルをHTMLに変換する処理を追加していきます。

[コラム] Node.jsのエラーファーストコールバック

Node.jsが提供するnode:fsモジュールは同期APIと非同期APIを提供するという話を紹介しました。 歴史的な経緯もあり、Node.jsではPromiseとエラーファーストコールバックの2種類の非同期APIを提供しているケースもあります。

node:fs/promisesモジュールでは、readFileメソッドは、Promiseを返す非同期APIでした。 一方で、node:fsモジュールにもreadFileメソッドがあり、このAPIはエラーファーストコールバックを扱う非同期APIです。

// fsモジュールにはエラーファーストコールバックを扱う非同期APIも含まれる
import * as fs from "node:fs/promises";

// エラーファーストコールバックの第1引数にはエラー、第2引数 には結果が入るというルール
fs.readFile("sample.md", (err, file) => {
    if (err) {
        console.error(err.message);
        process.exit(1);
        return;
    }
    console.log(file);
});

エラーファーストコールバックについては、非同期の章でも紹介しています。 エラーファーストコールバックは、PromisesがECMAScriptに入るES2015より前においては、非同期な処理を扱う方法として広く使われていました。 Node.jsの多くのモジュールは、ES2015より前に作られているため、node:fsモジュールのようにエラーファーストコールバックを扱うAPIもあります。

一方で、Promiseが非同期APIの主流となったため、Node.jsにもPromiseを扱うためのAPIが追加されました。 しかし、node:fsモジュールではエラーファーストコールバックを提供するメソッドと名前が衝突したため、fs.promisesというプロパティにPromiseのAPIがまとめられています。 このままではPromiseのAPIが使いにくいため、PromiseのAPIのみをもつモジュールとしてnode:fs/promisesが新たに追加されました。 また、Node.jsではエラーファーストコールバックを受け取る非同期APIをPromiseを返す非同期APIへとラップするutil.promisifyというメソッドも提供しています。

Node.jsでは、歴史的な経緯からエラーファーストコールバックとPromiseのAPIがどちらも提供されていることがあります。 しかしながら、両方が提供されている場合はPromiseのAPIを利用するべきです。 Promiseを扱うAPIには、他のPromiseを扱う処理との連携のしやすさ、Async Functionという構文的なサポート、エラーハンドリングの簡潔さなどのメリットがあります。

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

  • node:fs/promisesモジュールのreadFile関数を使ってファイルを読み込んだ
  • UTF-8形式のファイルの中身をコンソールに出力した
  • readFile関数の呼び出しにエラーハンドリング処理を記述した