【Node.js】 ワイルドカードを使用したファイル一覧取得
更新日:2021/10/04
ディレクト下のファイルをワイルドカードを使用して取得したいというケースは意外と多いです。
Node.jsにはファイル名を取得するメソッドがありますが、ワイルドカードでの判定ができません。
そこで今回は、何か良い方法がないか考えてみました。
globを使用した場合
ディレクトリ内のファイルをワイルドカードを使用してリストアップするときは、globモジュールが便利です。
globの準備
globは標準モジュールではないので、インストールする必要があります。
globのインストール
npm install glob
globの使い方
使用するときは、requireまたはimportで読み込みます。
globの読み込み
const glob = require("glob");
通常の使い方
単純にリストアップするとき、globの構文は次のようになります。
globの構文
glob( パス , コールバック関数 );
glob( パス , オプション , コールバック関数 );
コールバック関数は、エラーを示すフラグと、ファイル名(文字列)のリストを受け取ります。
使用例
glob( "*.txt" , function(err, files){
if(err) {
console.log(err);return;
}
console.log(files);
});
受け取るファイル名の形式は、指定したパスの形式に合わせられます。
ファイル名のみ: "*.txt" ⇒ ["abc.txt","123.txt"]
相対パス: "dir/*.txt" ⇒ ["dir/abc.txt","dir/123.txt"]
フルパス: "c:/data/dir\*.txt" ⇒ ["c:/data/dir/abc.txt","c:/data/dir/123.txt"]
ただし、オプションabsoluteを有効にした場合、フルパスになります。
globでは、パス区切りとして "/" を使用します。
Windowsでは"\"でも期待した結果を得られますが、"/"を使っておいた方がよさそうです。
ステータス等を取得する
ステータス等取得したい場合はGlobオブジェクトを作成します。
このときオプションのstatを有効にします。
ステータス等を取得
const mg = new glob.Glob(path , { stat:true } , ( err , files ) =>{
if(err) {
console.log(err);return;
}
console.log(files); // ファイル名の一覧
console.log( mg.statCache ); // ステータスの一覧
} );
statCacheは、次のような情報を持つオブジェクトです。
{ ファイルパス : Stats { dev: 1714223727, mode: 33206, nlink: 1, uid: 0, gid: 0, rdev: 0, blksize: 4096, ino: 80501843339343250, size: 1175, blocks: 8, atimeMs: 1632901106585.9707, mtimeMs: 1632898244510.8936, ctimeMs: 1632898263513.1685, birthtimeMs: 1592288684363.3054, atime: 2021-09-29T07:38:26.586Z, mtime: 2021-09-29T06:50:44.511Z, ctime: 2021-09-29T06:51:03.513Z, birthtime: 2020-06-16T06:24:44.363Z }, ファイルパス : Stats { }, ファイルパス : Stats { }, }
syncさせる
ファイルの検索終了を待ってから次のコードを実行させる場合、glob.syncを使用します。
コールバックは指定せずに、結果は戻り値で受け取ります。
ステータスを取得したい場合は、Globオブジェクトを作成してsyncオプションとstatオプションを有効にします。
syncでステータスを取得
const mg = new glob.Glob(path , { sync:true , stat:true } );
console.log( mg.statCache );
ディレクトリのみ取得する
パスの最後に "/" を指定するとディレクトリのみ取得できます。
ディレクトリのみ取得
glob( "*/" , function(err, files){
if(err) {
console.log(err);return;
}
console.log(files);
});
ファイルのみ取得する
ファイルのみ取得するには、nodirオプションを有効にします。
ファイルのみ取得
glob( "*" , {nodir:true} , function(err, files){
if(err) {
console.log(err);return;
}
console.log(files);
});
特定のファイルやディレクトリを除外する
特定のファイルやディレクトリを除外するには、ignoreオプションを指定します。
ignoreオプションは文字列の配列で、各要素はパターン指定できます。
ただし、ファイルやディレクトリの名称と一致する必要があります。
例: "abc.jpg" ⇒ abc.jpgは除外されるが、xxabc.jpgは除外されない
ファイルのみ取得
glob( "*" , {ignore:["abc.jpg","x-*.png"]} , function(err, files){
if(err) {
console.log(err);return;
}
console.log(files);
});
globのパス指定
第一引数には、検索するパスのパターンを指定します。
フルパスでも相対パスでも指定できます。
また、マッチングはファイル名だけでなく、パスにも適用されます。
例えばWindowsで次のようにパス指定したとします。
c:/Users/name/+(dir1|dir2)/*.txt
この場合、次の二つのフォルダが検索されます。
c:/Users/name/dir1/*.txt
c:/Users/name/dir2/*.txt
globのワイルドカード( * )などのパターンは、次のようになっています。
パターン | 例 | マッチ例 | 意味 |
---|---|---|---|
* | *.txt | abc.txt 、 .txt | 0個以上の文字列にマッチ |
? | a?c.txt | abc.txt 、 a1c.txt | 1文字にマッチ |
[ - ] | abc.mp[3-4] | abc.mp3 、 abc.mp4 | 範囲内の1文字にマッチ |
[! - ]または[^ - ] | abc.mp[!4-5] | abc.mp3 、 abc.mp6 | 範囲外の1文字にマッチ |
!( | | ) | abc.!(jpg|png) | abc.gif | 指定された文字列以外にマッチ |
+( | | ) | abc.+(jpg|png) | abc.jpg 、abc.png | 指定された文字列にマッチ |
?( | | ) | abc.?(jpg|png) | abc.jpg 、abc.png 、abc. | 指定された文字列が0回 または1回出現するとマッチ |
*( | | ) | abc.mp*(3|4) | abc.mp3 、abc.mp4 、 abc.mp 、abc.mp333 | 指定された文字列が 0回以上出現するとマッチ |
@( | | ) | abc.@(mp[3-4]|pdf|png) | abc.mp3 、abc.mp4 、 abc.pdf 、abc.png | パターンのどれかに 一致するとマッチ |
{ , } | a{bcde,xyz}.txt | abcde.txt 、 axyz.txt | 文字列にマッチ |
** | c:/User/**/abc.txt | c:/User/name1/abc.txt c:/User/name2/abc.txt | パスに**のみ指定した場合 シンボリックリンク以外の ディレクトリに一致 |
.txtなど"."で始まるファイルは、"*txt"や"*.txt"などで一致しません。
一致させたい場合は、オプション dot を true にセットする必要があります。
パターンをチェックする前に、{ }が展開されます。
a{bcde,b/dir/a,[1-9]}.txt
⇒ abcde.txt , a/dir/a.txt , a[1-9].txt
globのオプション指定
第二引数に関数以外のオブジェクトを指定すると、オプションとみなされます。
オプション指定例
const opt = {
dot : true,
silent : true,
nodir : true,
ignore : ["*.txt"]
};
glob( "*" ,opt , function(err, files){
if(err) {
console.log(err);return;
}
console.log(files);
});
プロパティ名 | 意味 | 型 | デフォルト |
---|---|---|---|
cwd | カレントディレクトリを指定 | 文字列 | process.cwd()の結果 |
root | パスのルート | 文字列 | path.resolve (オプション.cwd,"/")の結果 |
dot | "."で始まるファイルを一致させる | 真偽値 | false |
nomount | 指定パスが"/"で始まるとき、 結果にオプション.rootを結合しない | 真偽値 | false |
mark | ディレクトリの一致に/文字を追加する | 真偽値 | false |
nosort | 結果を並び替えない | 真偽値 | false |
stat | ステータスを取得する | 真偽値 | false |
silent | エラーを標準エラーに出力しない | 真偽値 | false |
strict | エラー時に中断する | 真偽値 | false |
cache | 以前のキャッシュを使用する | 真偽値 | false |
statCache | ステータスのキャッシュを使用する | 真偽値 | false |
symlinks | シンボリックリンクのキャッシュを使用する | 真偽値 | false |
sync | 検索完了を待つ | 真偽値 | false |
nounique | パターン展開時の ファイル重複チェックを無効にする | 真偽値 | false |
nonull | 空の結果を抑制しパターン 自体を含むセットを返す | 真偽値 | false |
debug | デバッグログを有効にする | 真偽値 | false |
nobrace | { }の展開を無効化 | 真偽値 | false |
noglobstar | "**"を"*"として扱う | 真偽値 | false |
noext | +(a|b) パターンを無効にする | 真偽値 | false |
nocase | 大文字と小文字を区別しない | 真偽値 | false |
matchBase | パスに"/"がない場合、 下位ディレクトリも対象とする | 真偽値 | false |
nodir | ファイルのみ一致 | 真偽値 | false |
ignore | 除外条件の指定。 こちらを参照。 | 文字列配列 | |
follow | **に一致するシンボリックリンク はディレクトリを参照する。 | 真偽値 | false |
realpath | fs.realpathを呼び出す。 | 真偽値 | false |
absolute | 結果を絶対パスで返す | 真偽値 | false |
fs | ファイルシステムオブジェクト | 組み込みfsモジュール |
自作コードで実装する
globは汎用的な機能を盛り込んでいるので、コード量がそれなりに多いです。
さらに第三者提供によるライブラリモジュールのため、不具合が混在している可能性を覚悟する必要があります。
ワイルドカードを使用したファイル一覧取得は、目的を限定すれば、それほど多くのコードを記述しなくてもよい場合が多いです。
そこで、次のような条件でコードを作成してみます。
条件
- 検索パスをフルパスで指定する
- 検索パスが"/"または"\"で終わる場合ディレクトリが指定したとみなし全ファイルを対象とする
- 検索パスのファイル名部分のみ次の二つのワイルドカード指定可能
*:0文字以上の任意の文字
?:任意1文字 - globの+( | | )パターンも使用可
- ファイルのみ取得する
- ステータスも取得する
2は、パスのステータスを取得してディレクトリかどうかを判断する方が妥当かもしれません。
しかし記事として掲載する関係上、少しでもコードを短くするためにこのような仕様になっています。
4はglobの他のパターンも検討したのですが、正規表現への変換が非常に難しく検証にも時間がかかるため、一番簡単なパターンのみ採用しています。パターンを増やしたいときは、素直にglobを使った方が良いと思います。
コード
ワイルドカードを使用したファイル一覧取得
const fs = require("fs/promises");
const fpath = require("path");
// エラー終了
const errorEnd = msg => {console.error(msg);process.exit(-1);}
const searchDir = async (searchPath) => {
// ①パスをディレクトリとファイル名に分割
// "/"で終わるならファイル名無しとみなす
const {dir,base} = searchPath.endsWith("/") || searchPath.endsWith("\\")
? { dir:searchPath , base:null } : fpath.parse( searchPath );
// ②ファイル名のチェックをおこなう正規表現オブジェクト作成
const fileMatch = base === null ? null
: new RegExp("^"
+ base.replace(/(\+)(\(.*?\))|([*?+.{}()\[\]^$|])/g,
(...s)=> {
switch (s[1]===undefined ? s[0] : s[1]){
case "+" : return s[2];
case "*" : return ".*?";
case "?" : return ".";
default: return "\\"+s[0];
}
} )
+ "$");
// ③ディレクトリ内のファイルを取得
const files = await fs.readdir(dir,{ withFileTypes:true }).catch(e=>({err:e}));
if( files.err ) errorEnd( files.err.message );
// ④対象ファイルを抽出
const result = files.filter( dirent=>{
// ファイルかどうか
if( !dirent.isFile() ) return false;
// 正規表現によるマッチング
return fileMatch === null ? true
: fileMatch.test( dirent.name );
}).map( // ⑤ステータスの取得
dirent =>fs.stat( fpath.join( dir , dirent.name ) )
.then(
status => ({name:dirent.name,stat:status})
,e=> ({name:dirent.name,error:e})
)
);
// ⑥ステータスの取得を待つ
const statusList = await Promise.all( result );
console.log( statusList );
};
searchDir関数は、パスを引数として受け付けます。
使用例
searchDir( "c:\\user\\xxxx\\*.png" );
なお、このコードはWindows10のみでテストしています。
他のOSでも動作するはずですが、保証はしていません。
簡単な解説
ファイル取得処理を関数化してあります。
この関数内で awaitを使用したかったので、関数にasyncを付与しています。
async/awaitについては、次のページを読んでみてください。
■【JavaScript】 async/awaitを解説します
パスのチェック
関数内の①は、指定されたパスをディレクトリとファイル名に分割しています。
ただしパスをディレクトリと判断した場合は、ファイル名が null になります。
ファイル名を正規表現にコンバート
②が、今回の関数で一番難しい箇所です。
ワイルドカードが含まれたファイル名を、正規表現のパターンに変換しています。
コード上では、ファイル名を次のような正規表現パターンでマッチさせています。
/(\+)(\(.*?\))|([*?+.{}()\[\]^$|])/g
このパターンは、+( ・・・ )のとき、"+"と、"( ・・・ )"がキャプチャされます。
2番目の"( ・・・ )"は正規表現の条件一致パターンとみなせるので、一番目の"+"だけ削除します。
*?+.{}()\[\]^$|のいずれかのとき、各文字がキャプチャされます。
"*"は、0文字以上をマッチさせる".*?"に置き換えます。
"?"は、一文字をマッチさせる"."に置き換えます。
他は正規表現のメタ文字なので、\でエスケープしています。
ファイル名読み込み
③は、ディレクトリ下のファイル名を読み込んでいます。
コードの冒頭で"fs/promises"をrequireしているので、fs.readdirはPromiseを返します。
返ってきた値にawaitキーワードが作用して、Promiseの結果が出るまで次のコードの実行を待ちます。
なお、後ろに続くcatchはエラーを捕捉して、ここで返された値がawaitの結果となります。
またfs.readdirのwithFileTypesオプションをtrueにすると、ファイルの種類を特定できるfs.Direntオブジェクトの配列が結果として返ります。
ファイルかどうかをチェックしたいとき、便利です。
対象ファイルの抽出
④は、③で取得したファイル情報から条件に合うものを抽出しています。
Arrayのfilterメソッドは、各要素に対してコールバック関数を呼び出して、その結果がtrueのもので新しい配列を作成します。
コールバック関数内では、まずはファイルかどうかを確認しています。
次に正規表現のマッチングテストをおこなっています。
ステータスの取得
⑤は、④で抽出したファイルのステータスを取得するPromiseオブジェクトを作成しています。
ここではmapメソッドを使用しているので、Promiseオブジェクトの配列が生成されます。
fs.statが作成したPromiseはステータスのみ返します。
そのままではファイル名が消えてしまうので、thenでファイル名を含んだオブジェクトを作成して返しています。
こうすることで、Promiseの結果がそのオブジェクトになります。
なおthenでエラーを捕捉していますが、エラーが発生する状況を再現できなかったため、この部分に関してはテストできていません。
ご了承ください。
結果待ち
⑥は、⑤で生成したPromiseオブジェクトの結果を待ちます。
Promise.allは配列中のPromiseのうち一つでも拒否されると、その時点で結果が返ります。
しかしこのコードでは、thenの2番目の引数で拒否を受け取って値を返すことで、拒否されたことを打ち消しています。
そのため最終的には拒否されないので、全ての実行結果を得ることができます。
最終的な結果として、次のような配列を取得できます。
[ { name: 'xxxxx.xx', stat: Stats { dev: 1714223727, mode: 33206, nlink: 1, uid: 0, gid: 0, rdev: 0, blksize: 4096, ino: 7036874418757113, size: 2656, blocks: 8, atimeMs: 1633330217113.4949, mtimeMs: 1633330146693.6936, ctimeMs: 1633330160880.8123, birthtimeMs: 1632902753072.5093, atime: 2021-10-04T06:50:17.113Z, mtime: 2021-10-04T06:49:06.694Z, ctime: 2021-10-04T06:49:20.881Z, birthtime: 2021-09-29T08:05:53.073Z } }, { name: 'xxxxxx',stat: Stats { } }, { name: 'xxxxxx',stat: Stats { } }, ]
更新日:2021/10/04
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。