Top  上へ  目次



ワイルドカードのマッチング関数(番外編)



 ちょっと前ですが、困った質問がありました。

Q.○○というファイラー(よそ様のソフト)でワイルドカード検索をした場合にヒットしないことがあるのですが、原因がお分かりになるでしょうか?
A.・・・(心の声:そのソフトの作者に聞けや!知るかボケェ!)

 こんな問い合わせがありました。
 完全にお門違いの問い合わせであり、私が回答すべき内容ではないので完全スルーしましたが、だいたいの原因が推測できたのが今回の記事の発端です。

 原因は、わりと古くからワイルドカードのパス表記の一致を見るのに使われている、有用性の高いWindows APIの動作が変わってる事だと思います。
 具体的にはPathMatchSpec()というAPIです。

 このAPIはWindows 8正式版の頃から挙動が怪しくなりまして、取りこぼしやら誤ヒットやらと、落ちるわけじゃないんですが比較の結果が正しくない文字が多数発生してました。
 うちも、しばらくNG文字だけ個別で判定させてたんですが、NG文字を調べきれなくなってきたのと、OSバージョンが進むにつれてNG法則が増えてきて良く分からなくなってきたんですよ。
 というわけで、ウチではこのAPIと互換っぽいモノをまるっと自作しています。


 で、知り合いのFTPツールの作者さんとお茶しながら「こういう問い合わせって困るよねぇ」なんて話をしてたら
 「はぁ?PathMatchSpec()の不具合?やべぇ!仕事で使ってんだけど!ソース見せてくださいませ(ジャンピング土下座からの五体投地)」という話の流れで横展開していく事になりました。

 そもそも、ウチでこの問題が解決できているのもウチの掲示板で報告があって私も気がついたものですし、ユーザーさんのおかげです。
 というわけで、世の中に少しでも還元できたら幸せになれる人がちょっぴり増えるかもしれないというわけで、上記のファイラーの作者さんと連絡とってみたり、公開記事にしてみたりという話の流れです。
 この手のソースコード自体は古典に分類されるものですが、わりと便利に使えるものなので必要な方はどうぞという感じです。
 まぁ、気が向いたので公開っつーことで、次を期待しないでください。
 このコードそのものは20年以上前に書いたものの焼き直しですが、古典なのでググれば似たようなモノも出てくると思います。


 あと一応、ツッコミがあるかもしれないので先に補足しておきます。
 Windows Vista以降であればPathMatchSpecEx()というAPIが別で存在しているので、そっちを使うのも良いかもしれません。
 私は深追い調査をしてないので、どっちが良いとか調べてませんが、ソフトがXP以前をフォローしていると動的リンクしなきゃいけないけません。
 また、Vista以降専用であるはずのAs/RでPathMatchSpecEx()を使ってない理由は、MS社さんに散々振り回されて嫌気がさしたんで察してください。(苦笑)
 まぁ、ウチのファイラーだと仮想名称でもフィルタリングが可能なので、MAX_PATHより長い文字列を比較する必要があり、どうせ自作の関数も必要になってたという話もありますしね。

 あとついでに余談ですが、FindFirst系のAPIだとワイルドカードの組み合わせによって、要らない候補を引っ掛けてしまうという問題があると思います。
 例えば8文字で~(チルダ)を含んでいたら、ショートファイル名で引っ掛けちゃったりするので、こういう関数で事前に弾いておく必要があります。
 文字列のコピー関数で、コピーした文字数と~(チルダ)の有無を返せというのはここで効いてきますが、ちゃんと使ってるよね?
 (To ファイラー作成中の新人作者の方)




 今時のコードなので、UNICODE版のみです。
 そのわりに2バイトのUNICODE文字しか考慮してませんので、4バイト文字とかはフォローしきれていません。
 お魚の「ホッケ」とか、草「なぎ」剛さんとかNGなのでご注意ください。
 (必要なら文字コードをUTF8とかUTF32で処理しちゃえば実現できますが、性能上の理由でウチでは採用しませんので悪しからず)

 MFC使ってますが、コアの部分はC言語なので、移植にも迷うことはないと思います。
 あと曖昧さ回避のため__wchar_tを使えとか、LPWSTR使えよとかあるでしょうけど、あくまでサンプル提供なんで気にしないでください。
// デリミタで区切られたテキストを分解して配列に格納する
//(うちの仕様にあわせた空も分割版、PathMatchSpecXだけで使うならコメント解除した方が効率良いでしょう)
// pArray : 分割後の配列
// strString : 分割対象文字列
// cTarget : デリミタ
int TokenSearch(CStringArray *pArray, CString strString, TCHAR cTarget)
{
	int nMax = strString.GetLength();
	if (nMax == 0)
		return 0;
	wchar_t *szToken = new wchar_t[nMax + 1];
	szToken[0] = 0;
	int nCounter = 0;
	for (int i = 0; i < nMax; i++)
	{
		if (strString[i] == '\0')
			break;
		if (strString[i] == cTarget)
		{
			//if (szToken[0] != 0)
			pArray->Add(szToken);
			nCounter = 0;
		}
		else
			szToken[nCounter++] = strString[i];
		szToken[nCounter] = 0;
	}
	//if (szToken[0] != 0)
	pArray->Add(szToken);
	delete []szToken;
	return (int)pArray->GetSize();
}

// ワイルドカードパターンマッチ(複数条件指定不可)
// lpszString : 検索文字列
// lpszSpec : ワイルドカードを含む文字列 
BOOL _MatchSpec(LPCWSTR lpszString, LPCWSTR lpszSpec)
{
	//どこかNGになるまで再帰でチェック、パス前提の文字分布で頻出順に判定
	switch (*lpszSpec)
	{
	case '\0'://終端なので成功で再帰を抜ける
		return (*lpszString == '\0');
	case '*'://次の候補が一致か、文字列終端でない&比較の次が一致
		return _MatchSpec(lpszString, lpszSpec + 1) || ((*lpszString != '\0') && _MatchSpec(lpszString + 1, lpszSpec));
	case '?'://終端でない&それぞれの次が一致
		return (*lpszString != '\0') && _MatchSpec(lpszString + 1, lpszSpec + 1);
	default:
		return (*lpszString == *lpszSpec) && _MatchSpec(lpszString + 1, lpszSpec + 1);
	}
}

//セパレータ付きのワイルドカード文字列のパターンマッチ、PathMatchSpec()の置換え用
//Windows8でPathMatchSpecが嘘(首とかマッチしない)を返す対策
BOOL PathMatchSpecX(LPCWSTR szCheckString, LPCWSTR szWildcard)
{
	//全部小文字で判定
	CString strWildCard = szWildcard;
	strWildCard.MakeLower();
	CString strCheckString = szCheckString;
	strCheckString.MakeLower();

	CStringArray arr;
	int nCount = ::TokenSearch(&arr, strWildCard, ';');//ワイルドカード指定文字列を;で分割、Web検索風味ならスペース分割とかでも良いかと
	for (int i = 0; i < nCount; i++)
	{
		if (_MatchSpec(strCheckString, arr.GetAt(i)))
			return TRUE;
	}
	return FALSE;
}