正規表現を使ったコマンドラインのパース

試験用とかデモ用とかでコマンドラインインタプリタもどきを作る場合、コマンドラインのパースがちと面倒です。

>cmd opt1 opt2 opt3 opt4
  • >cmd
  • >opt1
  • >opt2
  • >opt3
  • >opt4

このように単純な場合はスペースで分割、でいいのですが、

>cmd opt1 opt2 "opt 3" o"pt"4
  • >cmd
  • >opt1
  • >opt2
  • >opt 3
  • >o"pt"4

こういうダブルクォートを使ってスペースを含んだ引数もちゃんと扱う、ということになると単純な文字列分割では手に負えなくなります。
先頭から1文字ずつ見て分割処理する手もありますが、ぶっちゃけ面倒です。

なので正規表現をつかってサクッと分割する方法をメモしておきます。

まず、コマンド部分とオプション部分を分離します。

^([a-zA-Z][\w-]*)(\s.*)?$

最初のグループがコマンド部分、次のグループがオプション部分に対応します。
この正規表現では、コマンド部分の開始文字が英字という制約が付きますが、そこはお好みで弄ってください。

次にオプション部分を分割します。

\s+("(?:\\"|[^"])*"?|[^"]\S*)

この正規表現で1つのオプションが切り出せるので、Java だと Matcher.find() で順番に、perl だと m/〜〜/g で一気に切り出してやります。
この正規表現の場合、ダブルクォートで挟まれているオプションはダブルクォートを含んだまま切り出されるのと、オプション内でエスケープされているダブルクォートはそのままなので、後処理として

  1. 切り出されて文字列の先頭と末尾がダブルクォートの場合は取り除く(substr 系で)
  2. エスケープされているダブルクォートをアンエスケープ (文字列置換 \" -> ")

という処理を行えば目的が果たせます。


もう1つ、前後のダブルクォートを除いて切り出す正規表現として、

\s+(?:"((?:\\"|[^"])*)"?|([^"]\S*))

という手もあります。

こちらの場合は、マッチの結果、グループが空になる場合があるのでそれを無視することと、後処理としてエスケープされているダブルクォートのアンエスケープが必要になります。


以下、perlでの簡単な例。

# コマンドライン例
$testpattern = "cmd arg1 arg2 \"arg 3\" arg4 a\\\"rg\\\"5 \"arg6 arg7";

# コマンドとオプションの分離
print "parse cmd and args.\n";
@group = $testpattern =~ m/^([a-zA-Z][\w-]*)(\s.*)?$/;
$cmd = $group[0];
$args = $group[1];

print "cmd -> \"$cmd\"\n";
print "args-> \"$args\"\n";
print "\n";

# オプションの分割(type1)
print "parse args (type1).\n";
@group = $args =~ m/\s+("(?:\\"|[^"])*"?|[^"]\S*)/g;
foreach $arg (@group) {
  $arg = substr($arg, 1) if (index($arg, "\"") == 0);
  $arg = substr($arg, 0, -1) if (rindex($arg, "\"") == length($arg) - 1);
  $arg =~ s/\\"/"/g;
  print "-> \"".$arg."\"\n";
}
print "\n";

# オプションの分割(type2)
print "parse args (type2).\n";
@group = $args =~ m/\s+(?:"((?:\\"|[^"])*)"?|([^"]\S*))/g;
foreach $arg (@group) {
  next if ($arg eq "");
  $arg =~ s/\\"/"/g;
  print "-> \"".$arg."\"\n";
}

Java だとこうなる。エスケープてんこ盛りで可読性少々悪化。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Foobar {
    public static void main(String[] argv) {
        // コマンドライン例
        String testpattern = "cmd arg1 arg2 \"arg 3\" arg4 a\\\"rg\\\"5 \"arg6 arg7";
        Pattern cmd_arg_divide = Pattern.compile("([a-zA-Z][\\w-]*)(\\s.*)?");
        Pattern arg_parse1 = Pattern.compile("\\s+(\"(?:\\\\\"|[^\"])*\"?|[^\"]\\S*)");
        Pattern arg_parse2 = Pattern.compile("\\s+(?:\"((?:\\\\\"|[^\"])*)\"?|([^\"]\\S*))");

        // コマンドとオプションの分離
        System.out.println("parse cmd and args.");
        Matcher m = cmd_arg_divide.matcher(testpattern);
        if (!m.matches()) {
            System.err.println("Unmatch");
            return;
        }

        String cmd = m.group(1);
        String args = m.group(2);

        System.out.println("cmd -> \"" + cmd + "\"");
        if (args == null) {
            System.out.println("args-> no args");
            return;
        }
        System.out.println("args-> \"" + args + "\"");
        System.out.println();

        // オプションの分割(type1)
        System.out.println("parse args (type1).");
        m = arg_parse1.matcher(args);
        while (m.find()) {
            for (int i=1 ;i<m.groupCount()+1;i++) {
                if (m.group(i) != null) {
                    String arg = m.group(i);
                    if (arg.startsWith("\"")) {
                        arg = arg.substring(1);
                    }
                    if (arg.endsWith("\"")) {
                        arg = arg.substring(0, arg.length()-1);
                    }
                    arg = arg.replaceAll("\\\\", "");
                    System.out.println("-> \"" + arg + "\"");
                    break;
                }
            }
        }
        System.out.println();

        // オプションの分割(type2)
        System.out.println("parse args (type2).");
        m = arg_parse2.matcher(args);
        while (m.find()) {
            for (int i=1 ;i<m.groupCount()+1;i++) {
                if (m.group(i) != null) {
                    System.out.println("-> \"" + m.group(i).replaceAll("\\\\", "") + "\"");
                    break;
                }
            }
        }
    }
}