クモのようにコツコツと

フロントエンドエンジニア イイダリョウの技術ブログ。略称「クモコツ」

【gulp】gulp-replaceの引数に書かれている正規表現を掘り下げる

gulpの続きです。前回、HTMLテンプレート(EJS)、AltCSS(Sass(SCSS))、AltJS(TypeScript)を同時にコンパイルしました。しかしEJSの空行を無くすgulp-replaceが動いていないようなので掘り下げてみます。正規表現の書き方を調べる機会になりました。それでは行きましょう!

【目次】

※参考:【gulp】メタ言語(EJS、Sass(SCSS)、TypeScript)を同時コンパイルする! - クモのようにコツコツと

※メタ言語(HTMLテンプレートエンジン、AltCSS、AltJS)まとめ
qiita.com

前回のおさらい(EJSコンパイル)

前回EJSをコンパイルしたのがこちら。

f:id:idr_zz:20200501054310j:plain

コンパイル前のEJSファイル

other.ejs

<% var h1Title = '別ページ:EJSでフィズバス'; %>
<% var h2Title = 'EJSでフィズバズる'; %>
<% var count = 30; %>

<!doctype html>
<html>
<head>
   <meta charset="UTF-8">
   <title><%= h1Title %></title>
   <link rel="stylesheet" href="css/common.css">
   <link rel="stylesheet" href="css/other.css">
   <script src="js/common.js"></script>
</head>

<body>
    <section id="scss">
        <h1><%= h1Title %></h1>
        <p>別ページもリロードされるかテストですと。</p>
        <section>
            <h2 class="oya"><%= h2Title %></h1>
            <p>EJSで変数、for文、if文を使ってフィズってバズってみた。</p>
            <ul>
              <% for (var i = 1; i < count+1; i++) { %>
                  <% if (i % 3 == 0 && i % 5 == 0) { %>
                  <li>フィズってバズった!</li>
                  <% } else if (i % 3 == 0) { %>
                  <li>フィズった!</li>
                  <% } else if (i % 5 == 0) { %>
                  <li>バズった!</li>
                  <% } else { %>
                  <li><%= i %></li>
                  <% } %>
              <% } %>
            </ul>
          </section>
        <p class="lede"><a href="/">Topへ</a></p>
        </section>
    </section>
</body>
</html>

コンパイル後のHTMLファイル

other.html




<!doctype html>
<html>
<head>
   <meta charset="UTF-8">
   <title>別ページ:EJSでフィズバス</title>
   <link rel="stylesheet" href="css/common.css">
   <link rel="stylesheet" href="css/other.css">
   <script src="js/common.js"></script>
</head>

<body>
    <section id="scss">
        <h1>別ページ:EJSでフィズバス</h1>
        <p>別ページもリロードされるかテストですと。</p>
        <section>
            <h2 class="oya">EJSでフィズバズる</h1>
            <p>EJSで変数、for文、if文を使ってフィズってバズってみた。</p>
            <ul>
              
                  
                  <li>1</li>
                  
              
                  
                  <li>2</li>
                  
              
                  
                  <li>フィズった!</li>
                  
              
                  
                  <li>4</li>
                  
              
                  
                  <li>バズった!</li>
                  
              
                  
                  <li>フィズった!</li>
                  
              
                  
                  <li>7</li>
                  
              
                  
                  <li>8</li>
                  
              
                  
                  <li>フィズった!</li>
                  
              
                  
                  <li>バズった!</li>
                  
              
                  
                  <li>11</li>
                  
              
                  
                  <li>フィズった!</li>
                  
              
                  
                  <li>13</li>
                  
              
                  
                  <li>14</li>
                  
              
                  
                  <li>フィズってバズった!</li>
                  
              
                  
                  <li>16</li>
                  
              
                  
                  <li>17</li>
                  
              
                  
                  <li>フィズった!</li>
                  
              
                  
                  <li>19</li>
                  
              
                  
                  <li>バズった!</li>
                  
              
                  
                  <li>フィズった!</li>
                  
              
                  
                  <li>22</li>
                  
              
                  
                  <li>23</li>
                  
              
                  
                  <li>フィズった!</li>
                  
              
                  
                  <li>バズった!</li>
                  
              
                  
                  <li>26</li>
                  
              
                  
                  <li>フィズった!</li>
                  
              
                  
                  <li>28</li>
                  
              
                  
                  <li>29</li>
                  
              
                  
                  <li>フィズってバズった!</li>
                  
              
            </ul>
          </section>
        <p class="lede"><a href="/">Topへ</a></p>
        </section>
    </section>
</body>
</html>

改行が削除されずにスカスカになってるのはやはり見栄えが悪いのでなんとかしたい。

gulpfile.jsのEJSコンパイル部分

gulpfile.js

// EJSコンパイル
gulp.task('ejs',  (done) => {
    gulp.src(["./src/*.ejs", "!./src/_*.ejs"])
      .pipe(ejs({}, {}, { ext: ".html" }))
      .pipe(rename({ extname: ".html" }))
      .pipe(replace(/[\s\S]*?(<!DOCTYPE)/, "$1"))
      .pipe(gulp.dest("./dest/"));
    done();
});

詳細は前回記事を参照。

※参考:【gulp】メタ言語(EJS、Sass(SCSS)、TypeScript)を同時コンパイルする! - クモのようにコツコツと

この中の「gulp-replace」が動いていないっぽい。

gulp-replaceの設定部分

「gulp-replace」の設定部分

.pipe(replace(/[\s\S]*?(<!DOCTYPE)/, "$1"))

ここが動いてない。正規表現ぽいのが書かれている。ここを掘り下げてみる。

まずreplace()は文字の置換を行うメソッド。第一引数は置換前で第二引数は置換後。

replace('置換前', '置換後')

第一引数の部分は「正規表現」が書ける。正規表現を使うことで固有の文字列だけでない汎用的な処理ができる。

replace('/文字列(置換前)/フラグ', '文字列(置換後)')

※参考:gulp-replace - npm

正規表現の書き方

正規表現はこれまで雰囲気で使ってたのでちゃんと知っておきたい。こんな書き方。

/文字列(置換前)/フラグ
  • スラッシュとスラッシュで囲って最後にフラグを付ける。

正規表現のフラグは下記の3種類

フラグ 意味
g 1個目だけでなく最後まで検索します。
i 検索の際に大文字と小文字を区別しません。
m 複数行に渡って検索します。

※参考:replace()の機能のまとめ!『正規表現、function()、arguments、RegExp()』:JavaScript - モーリーのメモ

gulp-replaceの中の正規表現

gulp-replaceのreplace()の中を見ていく。

replace(/[\s\S]*?(<!DOCTYPE)/, "$1")

うーん、これは何だろう。パッと見では分かりにくい。

第一引数部分。

/[\s\S]*?(<!DOCTYPE)/

現状はスラッシュの後ろにフラグは何も付いてない。

文字のエスケープ:バックスラッシュ( \ )

まずは最初の角カッコ[ ]の中から。

\s\S

sSの前にバックスラッシュ\ がある。これは何か。

メタ文字を文字として検索したい場合は、その文字の前に『\(バックスラッシュ)』を付けます。

『\』は、次の1文字はメタ文字じゃないよ、という意味です。 『\』は、キーボードの『option + ¥』で入力出来ます。

※参考:replace()の機能のまとめ!『正規表現、function()、arguments、RegExp()』:JavaScript - モーリーのメモ

正規表現の処理が実行される文字を処理からエスケープしてテキストとして扱いたい場合にこれを使うんだな。

空白文字(\s、\S)

だが、今回の\s\Sのバックスラッシュ\は「s」や「S」のエスケープではなく「空白文字」という意味のようだ。

文字を表すメタキャラクタ

\s 空白文字(半角スペース、タブ文字他)にマッチ \S 空白文字 以外の文字 にマッチ

※参考:正規表現あれこれ - Qiita

いずれか一文字に一致([〜])

以下、正規表現の詳細「正規表現 メタ文字一覧」がわかりやすいのでこれを参考に調べていく。

※参考:正規表現サンプル集

\s\Sを囲っている角カッコ[〜]は「~のいずれか1文字」という意味になる。

[\s\S]

最長一致()と最短一致(?)

[\s\S]の後の*だと「最長一致」になるが…

直前のパターンの0回以上繰り返し(最長一致)

[\s\S]*

その後に?がついて*?になると「最短一致」になる。

直前のパターンの0回以上繰り返し(最短一致)

[\s\S]*?

最長一致、最短一致ってなんだ?

正規表現で繰り返しの「*」や「+」など指定した場合、できるだけ長い文字列を得ようとするのが「最長一致」、できるだけ短い文字列を得ようとするのが「最短一致」です。

※参考:正規表現サンプル集

パターングループ((~))

再び、「正規表現 メタ文字一覧」より。

その後のカッコ(~)は「パターングループ」

(<!DOCTYPE)

論理行末($)と部分置換($1)

第二引数(置換後)部分にも正規表現がある。

$1

$は「論理行末」。例えば。$は行末の「。」など。

しかし今回は$の前に何もないので違うっぽい。しかも$の後に数字の1がある。$1は「部分置換」のようだ。

「部分置換」についてはこちらがの解説がわかりやすい。

検索パターン:090-(\d{4}-\d{4})
置換文字列:080-$1

検索文字列で () で括った部分は置換後も変更が行われません。また、置換文字列の $1 で検索文字列の () を参照しています。このように部分置換を用いることで正規表現にマッチした文字列だけ置換を行うことができます。

なるほど!$1は第一引数の()を見に行っているわけか!

検索条件が複数あり、() で括られたグループが多くなった場合も $1, $2, $3 ... にように$で始まる参照用変数を増やしていくことで対応できます。

$11は一つ目のカッコという意味か。

※参考:正規表現における$1を使った部分置換の方法 | UX MILK

全体の意味

再び全体を見ると

.pipe(replace(/[\s\S]*?(<!DOCTYPE)/, "$1"))

<!DOCTYPEの直前の0文字以上の空白文字を<!DOCTYPEに置換する、という意味になる。

gulp-replaceの修正

doctypeを小文字に

ただ、ドキュタイプ宣言はそもそも小文字<!doctype html>だから一致しないのではなかろうか。

.pipe(replace(/[\s\S]*?(<!doctype)/, "$1"))
  • <!DOCTYPE<!doctypeに変更する

watchでgulpタスクが実行されるようにother.ejsを修正してみる

<% var h1Title = '別ページ:EJSでフィズバス!'; %>

h1Titleテキストに「!」を追加(以下、追加と削除を繰り返して検証する)

テキスト修正は反映された! f:id:idr_zz:20200501074923j:plain

しかし、「other.html」の空白は結果は変わらず。。

doctypeの削除

修正その2。

.pipe(replace(/[\s\S]*?/, ""))

<!doctype$1を削除する。そもそも適用範囲を<!doctype html>の前だけでなく全体の空行を削除したいため。

結果は、まだ変わらない。。

フラグを追加

修正その3

.pipe(replace(/[\s\S]*?/gmi, ""))

g``m``iの全部のフラグを追加してみる。

結果は変わらず…

空白行(^\n)に変更

修正その4

.pipe(replace(/^\n/gmi, ""))

空白行削除の^\nに変更

※参考:空行を削除する置換(正規表現) - Qiita

^は「行頭」、\nは「改行文字」という意味で合わせると改行のみの空白行、になる。

※参考:正規表現あれこれ - Qiita

しかしこれでも結果は変わらない。

単なる文字列の打ち替え(EJS→ejs)

あまりにも動かないのでもっとシンプルな内容にしてみるか。

.pipe(replace(”EJS”, "ejs"))

EJSという文字を小文字のejsにする。

うーむ、これでも変わらない。。

そもそも根本的にgulp-replaceが動いてない気がしてきた!!

gulp-replaceをグローバルインストール

gulp-replaceをローカルだけでなくグローバルにもインストールしてみるか。

いったん「Control + C」でサーバを閉じて、グローバルインストールする。

npm gulp-replace -g

で、またサーバを立ち上げて…

npx gulp

EJSファイルを修正すると…

おお!「EJS」が小文字の「ejs」になったあぁぁ!!! f:id:idr_zz:20200501090255j:plain

再び大文字に戻す!

.pipe(replace("ejs", "EJS"))

あれ、戻らんぞ??

サーバを再起動(gulpfile.fileはwatchしていない)

試しにもう一回「Control + C」でサーバを閉じてからgulp再起動する。

npx gulp

おお!戻った! f:id:idr_zz:20200509171929j:plain なるほど!つまり、gulpfile.js自体の修正はwatchしていないから、Nodeサーバ上のgulpfile.jsに修正が反映されないんだ!

だからサーバ起動中は変更が変わらない。一回サーバを閉じ流とgulpfile.jsが新しい内容になるわけか。

正規表現打ち替え再検証

再び検証する。(変更のたびにサーバ閉じるてgulp再起動、を繰り返している)

その1、空白文字の削除

.pipe(replace(/[\s\S]*?/gmi, ""))

→結果変わらず

その2、空白行の削除

.pipe(replace(/^\n/gmi, ""))

→結果変わらず

その3、空白行の削除フラグ付き

.pipe(replace(/^\n/gmi, ""))

おお!<!doctypeの前の空行はなくなった!

<!doctype html>
<html>
<head>
 /* 後略*/

しかし、ulタグの中のフィズバズ部分はまだ空行が…。

       <section>
            <h2 class="oya">EJSでフィズバズる</h1>
            <p>EJSで変数、for文、if文を使ってフィズってバズってみた。</p>
            <ul>
              
                  
                  <li>1</li>
                  
              
                  
                  <li>2</li>
                  
              
                  
                  <li>フィズった!</li>

                  /* 後略*/

ここはタブがかなり入っているから正規表現とマッチしないのかも。

タブとスペースのみの行の削除

タブも含んだ空行

調べたらこちらにタブも含んだ空行の書き方が!

^[ \t]*\n
  • ^は「行頭」
  • [〜]は「いずれか一文字に一致」
  • カッコの中は半角スペースとタブ\t
  • *は「最長一致」
  • \nは「改行」

これで半角スペースまたはタブと改行にマッチする行がマッチする。

※参考:タブや半角スペースだけの行も空行として削除 - 逆引き秀丸の正規表現で置換サンプル集 - DEARIE

空行が削除された!

replace()の第一引数にこれを入れる。

.pipe(replace(/^[ \t]*\n/gmi, ""))

サーバを閉じてgulp実行すると…

<!doctype html>
<html>
<head>
   <meta charset="UTF-8">
   <title>別ページ:EJSでフィズバス </title>
   <link rel="stylesheet" href="css/common.css">
   <link rel="stylesheet" href="css/other.css">
   <script src="js/common.js"></script>
</head>
<body>
    <section id="scss">
        <h1>別ページ:EJSでフィズバス</h1>
        <p>別ページもリロードされるかテストですと。</p>
        <section>
            <h2 class="oya">EJSでフィズバズる</h1>
            <p>EJSで変数、for文、if文を使ってフィズってバズってみた。</p>
            <ul>
                  <li>1</li>
                  <li>2</li>
                  <li>フィズった!</li>
                  <li>4</li>
                  <li>バズった!</li>
                  <li>フィズった!</li>
                  <li>7</li>
                  <li>8</li>
                  <li>フィズった!</li>
                  <li>バズった!</li>
                  <li>11</li>
                  <li>フィズった!</li>
                  <li>13</li>
                  <li>14</li>
                  <li>フィズってバズった!</li>
                  <li>16</li>
                  <li>17</li>
                  <li>フィズった!</li>
                  <li>19</li>
                  <li>バズった!</li>
                  <li>フィズった!</li>
                  <li>22</li>
                  <li>23</li>
                  <li>フィズった!</li>
                  <li>バズった!</li>
                  <li>26</li>
                  <li>フィズった!</li>
                  <li>28</li>
                  <li>29</li>
                  <li>フィズってバズった!</li>
            </ul>
          </section>
        <p class="lede"><a href="/">Topへ</a></p>
        </section>
    </section>
</body>
</html>

やた!ついに空白行削除が実現できた!

フラグが無いと削除されない

ちなみにフラグを無くすとどうなるのかな?

.pipe(replace(/^[ \t]*\n/, ""))
           <ul>
              
                  
                  <li>1</li>
                  
              
                  
                  <li>2</li>

                  /* 後略 */

ダメか…

最終結果

最終結果はこの状態!

// EJSコンパイル
gulp.task('ejs',  (done) => {
    gulp.src(["./src/*.ejs", "!./src/_*.ejs"])
      .pipe(ejs({}, {}, { ext: ".html" }))
      .pipe(rename({ extname: ".html" }))
      .pipe(replace(/^[ \t]*\n/gmi, ""))
      .pipe(gulp.dest("./dest/"));
    done();
});

最後に

正規表現は記号の羅列のように見えてついつい雰囲気だけで使ってたので、ちゃんと細かく内容を理解する機会になりました。

全体としては下記の3つの修正を行いました。

  • 「gulp-replace」をグローバルインストールする
  • Nodeサーバを閉じてgulp再実行(gulpfile.jsをwatchしてないため)
  • replace()の第1引数の正規表現をタブ付き空行削除に。フラグも付ける(/^[ \t]*\n/gmi
  • replace()の第2引数は空文字に

検証順でこうなったので、もしかしたらどれかの作業はやらなくても済むかもしれませんが、この3つをやっておけば間違いはないかと思います。

それではまた!


※メタ言語(HTMLテンプレートエンジン、AltCSS、AltJS)まとめ
qiita.com