Node.js で reduce 関数を使ってディレクトリを再帰的に作成する

Node.js でバッチ処理を作成する機会があり、あらためて [reduce 関数](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) が便利だなと実感したので投稿します。

Node.js でバッチ処理やサーバーサイドの処理を行う際、新規作成したディレクトリの中にファイルを書き出すといった処理が必要になることがあります。
作成するディレクトリがひとつの場合はまだ簡単ですが、存在するか判らない深い階層のディレクトリが必要な場合は再帰的に作成する処理が必要になります。
（事前に `.gitkeep` などでディレクトリを用意しておくことは考えないとして。）

今までは自作の再帰関数で処理していましたが、 __reduce 関数__ を使うと以下のように記述することができました。

```js:sample.js
const fs = require('fs');
const path = require('path');
const dist = path.join(__dirname, 'foo/bar/baz/dist');

dist
  .replace(__dirname + path.sep, '')
  .split(path.sep)
  .reduce((acc, cv) => {
    const cd = path.join(acc, cv);
    try {
      fs.mkdirSync(cd);
      console.log(`Made directory: ${cd}`);
    } catch (err) {
      (err.code !== 'EEXIST' || !fs.statSync(cd).isDirectory())
        && console.log(err.toString());
    } finally {
      return cd;
    }
  }, __dirname);
```

## reduce 関数とは

> reduce() はアキュムレータと配列の各要素に対して（左から右へ）関数を適用し、単一の値にします。
> 
> 引用: Array.prototype.reduce() - JavaScript | MDN
> <https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce>

「単一の値にします。」の説明の通り、 `sample.js` の reduce 関数の結果は `dist` と同じ値が返りますが、「配列の各要素に対して（左から右へ）関数を適用」される仕組みを利用することで簡単に左（上の階層）から順番にディレクトリを作成することができます。
この時、 `initialValue` として第二引数に起点となるディレクトリのパスを渡すことに注意します。

`sample.js` と同じ階層に `foo` ディレクトリが存在しない状態で `sample.js` を実行すると以下の結果となります。

```sh
# Mac 上の `/Users/hoge/sample/` ディレクトリで実行したと仮定
$ node sample
Made directory: /Users/hoge/sample/foo
Made directory: /Users/hoge/sample/foo/bar
Made directory: /Users/hoge/sample/foo/bar/baz
Made directory: /Users/hoge/sample/foo/bar/baz/dist
```

`dist` から不要な部分を `replace(...)` で除外し、必要なパスを `split(...)` で配列化して処理していますが、当然ながら最初から配列で `reduce(...)` の処理に値を渡すことができれば記述量をさらに減らすことができます。

## mkdirSync 関数を使ったディレクトリの作成と try...catch 構文による例外処理

Node.js でディレクトリやファイルの作成について調べてみると `fs` モジュールの statSync 関数や exists 関数（ Deprecated ）で存在判定をした後で作成するといった記事を見かけますが、 `sample.js` では存在判定は行わずに mkdirSync 関数を使用して判り易く同期的にディレクトリを作成しています。

この場合、1回目の `sample.js` の実行時にはディレクトリが存在しないので問題ないのですが、2回目以降は mkdirSync 関数がエラーを返して処理が止まってしまうので、 try...catch 構文で例外処理を追加しています。

`catch (err) {...}` では、既にディレクトリがあった場合は何も表示しませんが、何らかの理由で同名のファイル（ディレクトリではない）が存在した場合にはエラーのメッセージが表示されます。

```sh
# '/Users/hoge/sample/foo/bar' に 'baz' ファイルが存在した場合
# Mac
$ node sample
Error: EEXIST: file already exists, mkdir '/Users/hoge/sample/foo/bar/baz'
Error: ENOTDIR: not a directory, mkdir '/Users/hoge/sample/foo/bar/baz/dist'

# Windows
> node sample
Error: EEXIST: file already exists, mkdir 'C:\Users\hoge\sample\foo\bar\baz'
Error: ENOENT: no such file or directory, mkdir 'C:\Users\hoge\sample\foo\bar\baz\dist'
```

今回はユーザーの操作がないバッチ処理の想定なので、エラーの表示が不要な場合は `catch (err) {...}` 部分を削除して `try {...} finally {...}` のみにしても良いかもしれません。
（先に statSync 関数で存在判定をする場合は必ず `catch` の処理は必要になります。）

## reduce 関数はやっぱり有用だった

使い回しの再帰関数を見ながらふと思いついて reduce 関数で書き換えてみましたが、とても簡潔に記述することができました。
[mkdirp](https://www.npmjs.com/package/mkdirp) や [fs-extra](https://www.npmjs.com/package/fs-extra) といったモジュールを利用すれば簡単かもしれませんが、自分が理解できる範囲で処理が記述できたので満足しています。

今後もどんどん reduce 関数など有用な関数を覚えて JavaScript が綺麗に記述できるようになりたいと思います。
何か間違いや不具合などあればご指摘ください。

#### 検証環境

* macOS High Sierra 10.13.4
    * Node.js: v8.11.1
    * npm: 6.0.1
* Windows10 Enterprise Evaluation (virtual machine)
    * Node.js: v8.11.1
    * npm: 5.6.0

#### 参考記事

* [reduce関数は結構有用っていうお話 - あと味](http://taiju.hatenablog.com/entry/20110331/1301535208)
* [Node.js での例外処理: (1) try ～ catch による例外処理 | まくまく Node.js ノート](https://maku77.github.io/nodejs/exception/try-and-catch.html)
* [fs.exists() が deprecated になった理由 - I'm kubosho_](http://blog.kubosho.com/entry/why-deprecated-of-fs-exists)
* [Node.js で ファイル / ディレクトリ の 存在を確認する 方法 - galife](https://garafu.blogspot.jp/2017/06/nodejs-exists-directory.html)
* [Array.prototype.reduce() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce)
* [try...catch - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/try...catch)