selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

JavaScriptで正規表現(入門レベル)

モチベーション

現在、iioptという画像圧縮ツールを作成しています。このツールの一つの機能として、git pre-commit にhookしてcommit対象の画像を抽出し、圧縮が必要であれば圧縮するという機能を開発中です。

本記事は、そこで学んだ正規表現の書き方について記載します。記事の作成にあたって、下記のサイトを参考にさせていただきました。

developer.mozilla.org

※ 今まであんまり正規表現に触ってこなかったゆるふわエンジニアの記事なので、ある程度触った方には学ぶことがないかと思います。そのような方は、回れ右して頂くか、間違ってる点とう発見したらコメント貰えると喜びます!

やりたいこと

git pre-commit 時に git diff --cached --name-status を実行したとき、下記の結果が得られたとします。

# git diff --cached --name-status
M       images/need_match.jpg
M       images/not_matchjpg
M       images/need_match.png
M       images/not_match.ts
D       not_match.jpg
R100    images/sample.jpg       not_match.jpg
A       need_match.jpg
M       need_match.png

このとき、頭文字がM,Aであり、かつ拡張子がpng, jpgのファイル名のみを取得したいです。なので、求める最終的なアウトプットは下記のようになります。

'images/need_match.jpg',
'images/need_match.png',
'need_match.jpg',
'need_match.png'

最終的に採用したコード

  const input = `
M       images/need_match.jpg
M       images/not_matchjpg
M       images/need_match.png
M       images/not_match.ts
D       not_match.jpg
R100    images/sample.jpg       not_match.jpg
A       need_match.jpg
M       need_match.png
`
  const re = /^[AM]\s.+\.(?:jpg|png)$/gm;
  const files = input.match(re).map((line) => {
    console.log(line);
    return line.replace(/^[AM]\s*/, "")
  });
  console.log(files);
'images/need_match.jpg',
'images/need_match.png',
'need_match.jpg',
'need_match.png'

パターンについて

頭文字の判断

まず、頭文字 A & Mのもののみ取得したいです。 その場合、正規表現/^[AM]/となります。 実際にコードを書いて確かめると、下記のような結果が得られます。

console.log(/^[AM]/.test('A       sample1.jpg'));
console.log(/^[AM]/.test('D       hoge.jpg'));
true
false

ここで把握するべきものは、^[]の2つです。

まず[] についてです。これは文字列の集合を意味し、囲まれた文字のいずれか 1 個にマッチします。[]の中では - を使えば範囲を指定することができます。[0-9]とすれば、0から9までの数値すべてにマッチします。

> /[A-Z]/.test('BC123')
true
> /[A-B]/.test('123')
false

次の^ は、入力の先頭にマッチします。例えば、'BA' という文字列があったときに、/A/はマッチしますが、/^A/ はマッチしません。

ということで /^[AM]/ という正規表現を用意すれば、頭文字 A or M の文字列にマッチさせることができます。

特殊文字\sでタブ or 空白にマッチさせる

頭文字 A or M にマッチさせることができたので、次は空白 or タブにマッチさせたい。ここで把握するべき要素は、\s である。

まず、'M hoge.jpg' にマッチする正規表現を書きたい。 このとき必要となるのが、特殊文字 \s であす。 \sはmdnのドキュメントにおいて、「スペース、タブ、改ページ、改行を含む 1 個のホワイトスペース文字にマッチ」するものと書かれています。

> /^[AM]\s/.test('Mhoge')
false
> /^[AM]\s/.test('M hoge')
true

末尾マッチとグループ化

これまでの内容で出来上がった正規表現は、 /^[AM]\s/となります。この正規表現からマッチする文字列は、 A hogehogeM hogehoge のような文字列です。

ここから、A hoge_hage/hogehoge.jpgM hogehage.png のような文字列にマッチさせる正規表現になるよう、仕上げていく必要があります。今回は ., +, と$、そしてグループ化です。

まず . ですが、これは改行文字以外のすべての文字列にヒットします。次に+は、直前の文字の1回以上の繰り返しにマッチします。よって、.+ は1文字以上何かの文字列があれば、全てにマッチさせることになります。

> /^[AM]\s.+/.test('A &&&hogeあ')
true

このように自由度高く受け入れられるようにした理由としては、ユーザーがどのような名前の画像を用意するかわからなかったため、受け入れられる文字列を幅広く設定したかったからです。iioptはCLIツールなので、自分で自分に悪意を持って操作しない限り、問題が起きる可能性が少ないと考えたためです。

次にjpg, pngの拡張子を持つパスを取得できるようにします。ここで重要になってくるのが、$です。まず、jpgの拡張子を持つファイルだけ取得できるようにしましょう。$入力の末尾にマッチする特殊文字なので、/.jpg$/ のように書くと、hogehoge.jpg などのファイルパスにマッチできるようになります。これまで正規表現とくっつけると、下記のようになります。

> /^[AM]\s.+\.jpg$/.test('A &&&/hoge')
false
> /^[AM]\s.+\.jpg$/.test('A &&&/hoge.jpg')
true
> /^[AM]\s.+\.jpg$/.test('A &&&/hoge.jpga')
false

jpgだけでなくて、pngも取得したいです。ここでグループ化 という概念が出てきます。

正規表現 - JavaScript | MDN

グループ化は、(?: PATTERN) といった書き方で使います。簡単な例を以下に記載します。

 // "foo"で1つのパターンとして認識するため、"foofoo"がtrueになる
> /(?:foo){2,3}/.test("foofoo")
true
// "o" がパターンとして認識される。そのため、foofoo だとoが3つ続かないのでfalse
> /foo{2,3}/.test("foofoo")  
false
// "foooo" はfoの後に o が2~3続くのでtrue
> /foo{2,3}/.test("foooo") 
true

このグループ化を使うと、jpg, png の両方の拡張子に対応する正規表現は、/\.(?:jpg|png)$/ となります。 これまでの正規表現と繋げると、/^[AM]\s.+\.(?:jpg|png)$/ となって、ここのサイトを使って図にすると、下記の図のようになります。

f:id:selmertsx:20180130223827p:plain

※ この正規表現に、セキュリティ的に問題がある!などご存知の方がいましたら、ご指摘頂けると幸いです。

正規表現フラグ

/m フラグ

これまでの結果から、M need_match.png のような文字列が与えられたときに、マッチするために必要な正規表現/^[AM]\s.+\.(?:jpg|png)$/ であるということがわかりました。でも今回与えられる文字列は複数行あります。そういうときは、 /m フラグをつけましょう。複数検索ができるようになります。/mをつけて、確認した結果を以下に記載します。

  const input = `
M       images/not_matchjpg
M       images/need_match.jpg
M       images/need_match.png
M       images/not_match.ts
D       not_match.jpg
R100    images/sample.jpg       not_match.jpg
A       need_match.jpg
M       need_match.png
`
  const re = /^[AM]\s.+\.(?:jpg|png)$/m;
  const matches = input.match(re);
  console.log(matches);
[ 'M       images/need_match.jpg',
  index: 1,
  input: '\nM       images/need_match.jpg\nM ...

2行目の取得するべき文字列を取得してくれました。 が、3行目以降は検索しませんでした。 以上の結果から、/m だけでは、全ての行から一致するものを取得することができません。

/mg フラグ

そこで必要となってくるのが、/g フラグです。 これは、グローバルサーチ と呼ばれるもので、マッチした全ての文字列を取得することができます。 下記が実行結果になります。

// /g をつけないと最初に一致したもので終了
> `hage hoge fuga fugu hogo`.match(/fu\w*/);
[ 'fuga', index: 10, input: 'hage hoge fuga fugu hogo' ]
// /gを付けると、マッチする文字列全てを取得可能
> `hage hoge fuga fugu hogo`.match(/fu\w*/g);
[ 'fuga', 'fugu' ]

これらのフラグですが、組み合わせて使うことが可能です。ということで、/m と /gを組み合わせると下記のような結果になります。

  console.log('今回マッチさせたいもの');
  const input = `
Mimages/not_match.jpg
M       images/not_matchjpg
M       images/need_match.png
M       images/not_match.ts
D       not_match.jpg
R100    images/sample.jpg       not_match.jpg
A       need_match.jpg
M       need_match.png
A       &&HOGE/need_match.png
`
  const re = /^[AM]\s.+\.(?:jpg|png)$/gm;
  const files = input.match(re).map((line) => {
    return line.replace(/^[AM]\s*/, "")
  });
  console.log(files);
今回マッチさせたいもの
[ 'images/need_match.png',
  'need_match.jpg',
  'need_match.png',
  '&&HOGE/need_match.png' ]

ということで、欲しいデータだけ取得することができました。