ファイル操作

【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使用例


const files = glob.sync( "*.txt" );
console.log( files );

※二番目の引数にオプションを指定可能です

ステータスを取得したい場合は、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のワイルドカード( * )などのパターンは、次のようになっています。

globのパスパターン
パターンマッチ例意味
**.txtabc.txt 、 .txt0個以上の文字列にマッチ
?a?c.txtabc.txt 、 a1c.txt1文字にマッチ
[ - ]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}.txtabcde.txt 、 axyz.txt文字列にマッチ
**c:/User/**/abc.txtc:/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);
});

globのオプション
プロパティ名意味デフォルト
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
realpathfs.realpathを呼び出す。真偽値false
absolute結果を絶対パスで返す真偽値false
fsファイルシステムオブジェクト組み込みfsモジュール

 

自作コードで実装する

globは汎用的な機能を盛り込んでいるので、コード量がそれなりに多いです。
さらに第三者提供によるライブラリモジュールのため、不具合が混在している可能性を覚悟する必要があります。

ワイルドカードを使用したファイル一覧取得は、目的を限定すれば、それほど多くのコードを記述しなくてもよい場合が多いです。
そこで、次のような条件でコードを作成してみます。

条件

  1. 検索パスをフルパスで指定する
  2. 検索パスが"/"または"\"で終わる場合ディレクトリが指定したとみなし全ファイルを対象とする
  3. 検索パスのファイル名部分のみ次の二つのワイルドカード指定可能

    *:0文字以上の任意の文字
    ?:任意1文字

  4. globの+( | | )パターンも使用可
  5. ファイルのみ取得する
  6. ステータスも取得する

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

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。