MFCを使ったWindowsアプリの実装に関する話。
CWinAppを継承したクラスでは、m_lpCmdLineで起動時のパラメータを取得できる。そのパラメータは1個の文字列だが、半角スペースで区切られていて、半角スペースが内部に混入している場合はダブルコーテーションで括られるという特性がある。
シンプルな例で言うと、デスクトップにあるショートカットアイコンにファイルをドラッグ&ドロップした場合が該当し、そのファイルが半角スペース入りで複数あったら、けっこうメンドウなパラメータ解析が必要になるってこと。
例えばこんな単純な形であれば、半角スペースで区切ればいいだけなので余裕。
D:\work\test1.txt D:\work\test2.txt D:\work\test3.txt
しかし、こんな場合がやっかいこの上ない。
D:\work\test1.txt “D:\work\test1 – コピー (2).txt” “D:\work\test1 – コピー.txt”
ググればすぐに出てきて使えるだろうと考えて、以下記事のものをパクったのが間違いだった。
[MFC] Windowsアプリで引数を受け取る
上記のように、ファイル名に半角スペースが入っている場合にちゃんと分割してくれなかった。
そもそも、何でTrimを2回もやってんのか、最後に空の文字列が追加されてしまうので取り除くなんて書きっぷりが気に入らなかった。そのあたりはまぁ実害がないからいいとしても、ちゃんと分割されないことがあるってのはダメですな。
ということで、以下ちゃんと分割されるようにしたコードを掲載しておく。
void CCmdParamApp::IsCommand(CStringArray& ary)
{
// コマンドライン引数の取得
CString cmdParam(m_lpCmdLine);
cmdParam.Trim();
if (cmdParam.IsEmpty())
return;
// パラメータの分解
CString param;
int curPos = 0;
do {
if (cmdParam.GetLength() >= curPos && cmdParam.GetAt(curPos) == '\"') {
// "で括われた引数
++curPos;
param = cmdParam.Tokenize(_T("\""), curPos);
if (cmdParam.GetAt(curPos) == ' ') // "で閉じた後に半角スペースがあるから次へ進む
++curPos;
}
else {
// 引数を半角スペースで分解
param = cmdParam.Tokenize(_T(" "), curPos);
}
ary.Add(param.Trim());
} while (param != "");
// 最後に空の文字列が追加されてしまうので取り除く
ary.RemoveAt(ary.GetCount()-1);
}
最初のダブルコーテーションで括られた文字は問題ないが、2つ目で問題が発生する。1つ目の終わりのダブルコーテーションの次にスペースが来た場合(ある意味当然)に、2つ目の始まりのダブルコーテーションを識別できない。その結果、ダブルコーテーション中の半角スペースを区切り文字として認識してしまってアウト。
上記のように、終わりのダブルコーテーションの次が半角スペースだったら、半角スペースを読み飛ばして次に進めばよい。
扱う文字がファイル名やフォルダ名だったら、この処理で間違いないだろう。ダブルコーテーションが2つ続いたら、ダブルコーテーションそのものになるとか、エスケープ処理全般のことを考えたらもっとちゃんと考えなきゃいかんな。
そもそもTokenizeというCStringのメンバー関数が話をわかりにくくしている。MSの説明によると「ターゲット文字列内の次のトークンを検索します」ということで、まぁよく読めばわからないこともないが、こいつを組み込んだソースコードは判読性が低くなる。
ということで、以下はおいらが一から書いたパラメータ解析コード。文字を一文字ずつ見ていって、ダブルコーテーションか半角スペースかその他かで判別しているからわかりやすいでしょ。
void CCmdParamApp::IsCommand(CStringArray& ary)
{
// コマンドライン引数の取得
CString cmd(m_lpCmdLine);
cmd.Trim();
if (cmd.IsEmpty())
return;
CString param;
bool flg = false; // ダブルコーテーション内かどうか
for (int i = 0; i < cmd.GetLength(); i++)
{
TCHAR c = cmd.GetAt(i);
if (c == '\"')
flg = !flg;
else if (c == ' ')
{
if (flg) // ダブルコーテーション中なので、スペースをセパレータとして認識しない
param += c;
else // ダブルコーテーション外なので、スペースをセパレータとして認識する
{
if (!param.IsEmpty()) // 文字があるときだけ追加
ary.Add(param);
param.Empty();
}
}
else
param += c;
}
if (!param.IsEmpty()) // 最後の文字を追加
ary.Add(param);
}
なお、文字セットはUnicode使用のものである。イマドキ、文字セットにマルチバイトを使っているものはそうそうないと思うけど、もしそんな化石のようなコードがあったなら、日本語の1バイト目の判定を入れないとダメだからね。