ファイルを読み込む

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

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

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

sample.md

# sample

fsモジュール

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

fsモジュールは同期形式と非同期形式の両方が提供されています。 同期APIと非同期APIはどちらもfsモジュールに含まれていますが、 非同期形式のAPIはfs/promisesというモジュール名でも参照できるようになっています。 この書籍では分かりやすさのために、非同期形式のみのAPIを提供するfs/promisesモジュールを利用します。

Node.jsの標準モジュールはnode:fsのようにnode:プリフィックスをつけてインポートできます。 プリフィックスを付けないfsでもインポートできますが、npmからインストールしたサードパーティ製のモジュールとの区別が明確になるため、付けておくことが推奨されます。

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

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

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

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

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

次のサンプルコードは、指定したファイルを読み込む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);
});

そして、次のサンプルコードは、同じく指定したファイルを読み込む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にはfs/promisesモジュール以外にも多くの非同期APIがあるので、非同期処理に慣れておきましょう。

readFile関数を使う

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

main.js

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

// コマンドライン引数からファイルパスを取得する
program.parse(process.argv);
const filePath = program.args[0];

// ファイルを非同期で読み込む
fs.readFile(filePath).then(file => {
    console.log(file);
});

sample.mdを引数に渡した実行結果は次のようになります。 文字列になっていないのは、コールバック関数の第二引数はファイルの中身を表す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 { program } from "commander";
import * as fs from "node:fs/promises";

program.parse(process.argv);
const filePath = program.args[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 { program } from "commander";
import * as fs from "node:fs/promises";

program.parse(process.argv);
const filePath = program.args[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が提供するfsモジュールは同期APIと非同期APIを提供するという話を紹介しました。 歴史的な経緯もあり、Node.jsではPromiseとエラーファーストコールバックの2種類の非同期APIを提供しているケースもあります。

fs/promisesモジュールでは、readFileメソッドは、Promiseを返す非同期APIでした。 一方で、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より前に作られているため、fsモジュールのようにエラーファーストコールバックを扱うAPIもあります。

一方で、Promiseが非同期APIの主流となったため、Node.jsにもPromiseを扱うためのAPIが追加されました。 しかし、すでにエラーファーストコールバックを提供する同じ名前のメソッドがあったため、fsに対してfs/promisesのようにモジュールとして分けて扱えるようになっています。 また、Node.jsではエラーファーストコールバックを受け取る非同期APIをPromiseを返す非同期APIへとラップするutil.promisifyというメソッドも提供しています。

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

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

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