チリペヂィア

リンクフリー。サンプルコードなどは関連記事内でライセンスについて明示されない限り商用利用なども自由に行って構いませんが、自己責任でお願いします。またこれら日記内容の著作権自体は放棄していません。引用部分については引用元の権利に従ってください。

正規表現でコマンドライン文字列を分割

VB.NETをいじってまして、久しぶりに日記ネタを見つけました。

"実行ファイルパス" /s %d "半角 スペース" &H"やらしい区切り"&H

コマンドラインでよく見る単語の区切り方です。「スペースで区切るけど、ダブルクオーテーションで囲うとスペースも含めた単語としてみなしますよ」という文字列です。このパーサを作るのにWin標準のVBSにオマケでついてる正規表現エンジンでサクッと実装しちゃおうという腹なのです。

「今時、VBSでも.NETでも、コマンド引数なんて配列オブジェクトで渡してもらえるじゃないか」というご意見もっともなのですが、自前でやんなきゃいけなくなっちゃったのでしょうがないのです。しかし、絶対あると思ってたんですが「正規表現 コマンドライン」で検索しても意外と当たらないので私が書く事にします(グスン)。日記に書くネタがないので!早いもの勝ちだァ〜!

確認環境:Microsoft VBScript Regular Expressions 5.5@VB.NET2010
ヤツはプロジェクトプロパティの参照追加でCOMの中に居ます。ついでに言うと他の正規表現エンジンでどうなるかは正直ワカリマセン…あしからず。

個別に切り分ける時のマッチパターン "[^"]*?"|[^\s"]+

Dim objRegExp As New VBScript_RegExp_55.RegExp
objRegExp.Global = True ' 複数回ヒットを許可
objRegExp.Pattern = """[^""]*?""|[^\s""]+" ' VBでは文字列リテラル中のダブルクオーテーションを重ねて書くので"まみれになっています。
Dim objMatches As VBScript_RegExp_55.MatchCollection
objMatches = objRegExp.Execute("""実行ファイルパス"" /s %d ""半角 スペース"" &H""やらしい区切り""&H")
Debug.Print("解析スタート")
For Each objMatch As VBScript_RegExp_55.Match In objMatches
    Debug.Print(objMatch.Value)
Next

解析スタート
"実行ファイルパス"
/s
%d
"半角 スペース"
&H
"やらしい区切り"
&H
これだけ…ネタがなかったので笑って許してください。とくにサーバー管理系のPGなど、オシャレ上級者の方はご容赦くださいw

やたら短いのですが、どうにかなるものですね。ついでに、正規表現の記号をしょっちゅう忘れる自分のよく見る参考サイトを載せておきます。基本的な正規表現の記号の意味や、VBA等で使う時の参照設定はこちらを参照して下さい(私はよくVBSで使うのでVBAでは参照設定しないでCreateObject()しちゃうのですが)。
CodeZine VBAで正規表現を使う RegExpオブジェクトの利用

ちなみにダブルクオーテーション囲みをRegExpだけでどうにかしようと思ったら、こんな感じになりました。こうするとSubMatchesコレクションから持ってこれます。

()によるグループ化を使った""の削除パターン "([^"]*?)"|[^\s"]+

Dim objRegExp As New VBScript_RegExp_55.RegExp
objRegExp.Global = True ' 複数回ヒットを許可
objRegExp.Pattern = """([^""]*?)""|[^\s""]+"
Dim objMatches As VBScript_RegExp_55.MatchCollection
objMatches = objRegExp.Execute("""実行ファイルパス"" /s %d ""半角 スペース"" &H""やらしい区切り""&H")
Debug.Print("解析スタート")
For Each objMatch As VBScript_RegExp_55.Match In objMatches
    If objMatch.SubMatches(0) <> "" Then
        Debug.Print(objMatch.SubMatches(0))
    Else
        Debug.Print(objMatch.Value)
    End If
Next

解析スタート
実行ファイルパス
/s
%d
半角 スペース
&H
やらしい区切り
&H
以下、正規表現を使ったソースは二度とデバッグしたくない未来の自分に宛てて、詳しくないなりに説明を試みます。正規表現自体が初見の方は、上記参考サイトを片手にご覧ください。

[^\s"]+|"[^"]*?"

このパターンは、中心の|セパレータでOR条件になっている「[^\s"]+」あるいは「"[^"]*?"」にマッチするパターンという意味になります。ここで基本かつミソになるポイントですが、正規表現は標準では前から読んでいって適合しうる最長探査結果を探します。なので「[^\s"]+」は、スペースあるいはダブルクオーテーション以外の文字が出現してから、同様の条件に当てはまる文字が続く、できるだけ長い文字列にマッチするパターンになります(もしデフォルトで最短探査をすると、こういった正規表現パターンでは区切り文字の出現を待たずに一文字ずつ切り出してしまう事になるので、最長探査は自然なデフォルトオプションと言えます)。いわゆる普通のスペース区切りの単語を認識する正規表現です。

一方、「"[^"]*?"」は、ダブルクオーテーションに前後を囲まれた、ダブルクオーテーション以外の文字が0個以上続く文字列を認識するパターンです。ここで最短探査を指定する「?」記号を使っています。こうしないと、「"あばば"でゅーわー"ほげげ」のような文字列では、このパターンひとつで最長探査して「"あばば"でゅーわー"」まで認識されます。最短探査を指定することで、条件にあてはまる最短文字列「"あばば"」だけが認識されるようになります。

このパターンを指定してVB正規表現エンジンRegExp複数回ヒットを配列で返すようプロパティ設定して呼び出せば、うまいことSplit処理に成功するという算段になっています。

さてこのままでは、ダブルクオーテーションの囲みがくっついたまま渡されるのですが、それを削除するのはどうとでもなるでしょう。VBもTrim関数があります。今回せっかくなので、これも正規表現を使ってMatchCollection.SubMatchesコレクションで解決するサンプルを載せました。これについて説明します。

RegExp.Execute戻り値のMatchCollection型に含まれるSubMatchesは、正規表現の()でグループ化した部分を抽出・識別できます。注意点として、正規表現の文法構造的には()によるパターン化はネストできるのですが、VBRegExpの戻り値配列は動的にノードを持ったネストにはならないみたいです。イマイチ確信が持てませんが、軽くデバッグ出力してみた感じでは、返ってくるSubMatches()の構造は、もともと設定しておいた正規表現パターンによって項目数が固定される一次の配列として、おそらく先頭の「(」出現順で返ってきます。とにかく今回のように正規表現パターン中に1つの()が記述されていればSubMatches(0)の配列要素は必ず実体を持って返ってきます。しかしパターンがマッチしても()で抽出する部分パターンは、Or条件で適合しない片割れだったり長さ0でも良かったりする事があります。この場合は長さ0の確保済み文字列が返ります。

サンプルケースで言えば、ダブルクオーテーションで囲まれていない単語にマッチすると、SubMatches(0)にはインスタンス確保済みで長さ0の文字列型が返りました。この場合でも文字列型インスタンスは参照可能な実態が存在するので、配列の要素数や、ForEachのような構文だけでは部分マッチの成否は確認できません。参照した上で、空の""文字列と比較する必要があります。

オマケ

先頭の実行パスと、その他のコマンドライン文字列に二分するだけのサンプル

アプリから別アプリを起動したい時なんかはProcessStartInfoなどを使うかと思いますが、そういう時にはこっちの方法が使えると思います。

Dim objRegExp As New VBScript_RegExp_55.RegExp
objRegExp.Global = False ' 複数ヒットは無くてオッケー
objRegExp.Pattern = "(""([^""]*?)""|[^\s""]+)\s*(.*)"
Dim objMatches As VBScript_RegExp_55.MatchCollection
objMatches = objRegExp.Execute("""実行ファイルパス"" /s %d ""半角 スペース"" &H""やらしい区切り""&H")
Debug.Print("解析スタート")
If Not (objMatches Is Nothing) Then
    If objMatches(0).SubMatches(1) = "" Then
        Debug.Print("FileName:" & objMatches(0).SubMatches(0))
    Else
        Debug.Print("FileName:" & objMatches(0).SubMatches(1))
    End If
    Debug.Print("Argments:" & objMatches(0).SubMatches(2))
End If

解析スタート
FileName:実行ファイルパス
Argments:-s %d "半角 スペース" &H"やらしい区切り"&H