正则表达式的编译结构
Kobi's answer 是关于 "^\\d{1}{2}$" 或 "{1}" 案例的 Java 正则表达式(Sun/Oracle 实现)的行为。
下面是"^\\d{1}{2}$"的内部编译结构:
^\d{1}{2}$
Begin. \A or default ^
Curly. Greedy quantifier {1,1}
Ctype. POSIX (US-ASCII): DIGIT
Node. Accept match
Curly. Greedy quantifier {2,2}
Slice. (length=0)
Node. Accept match
Dollar(multiline=false). \Z or default $
java.util.regex.Pattern$LastNode
Node. Accept match
看源码
根据我的调查,该错误可能是由于 { 未在私有方法 sequence() 中正确检查。
方法sequence() 调用atom() 来解析原子,然后通过调用closure() 将量词附加到原子,并将所有带闭包的原子链接到一个序列中。
例如,给定这个正则表达式:
^\d{4}a(bc|gh)+d*$
然后对sequence() 的顶级调用将接收^、\d{4}、a、(bc|gh)+、d*、$ 的编译节点并将它们链接在一起。
带着这个想法,让我们看看sequence()的源代码,复制自OpenJDK 8-b132(Oracle使用相同的代码库):
@SuppressWarnings("fallthrough")
/**
* Parsing of sequences between alternations.
*/
private Node sequence(Node end) {
Node head = null;
Node tail = null;
Node node = null;
LOOP:
for (;;) {
int ch = peek();
switch (ch) {
case '(':
// Because group handles its own closure,
// we need to treat it differently
node = group0();
// Check for comment or flag group
if (node == null)
continue;
if (head == null)
head = node;
else
tail.next = node;
// Double return: Tail was returned in root
tail = root;
continue;
case '[':
node = clazz(true);
break;
case '\\':
ch = nextEscaped();
if (ch == 'p' || ch == 'P') {
boolean oneLetter = true;
boolean comp = (ch == 'P');
ch = next(); // Consume { if present
if (ch != '{') {
unread();
} else {
oneLetter = false;
}
node = family(oneLetter, comp);
} else {
unread();
node = atom();
}
break;
case '^':
next();
if (has(MULTILINE)) {
if (has(UNIX_LINES))
node = new UnixCaret();
else
node = new Caret();
} else {
node = new Begin();
}
break;
case '$':
next();
if (has(UNIX_LINES))
node = new UnixDollar(has(MULTILINE));
else
node = new Dollar(has(MULTILINE));
break;
case '.':
next();
if (has(DOTALL)) {
node = new All();
} else {
if (has(UNIX_LINES))
node = new UnixDot();
else {
node = new Dot();
}
}
break;
case '|':
case ')':
break LOOP;
case ']': // Now interpreting dangling ] and } as literals
case '}':
node = atom();
break;
case '?':
case '*':
case '+':
next();
throw error("Dangling meta character '" + ((char)ch) + "'");
case 0:
if (cursor >= patternLength) {
break LOOP;
}
// Fall through
default:
node = atom();
break;
}
node = closure(node);
if (head == null) {
head = tail = node;
} else {
tail.next = node;
tail = node;
}
}
if (head == null) {
return end;
}
tail.next = end;
root = tail; //double return
return head;
}
记下throw error("Dangling meta character '" + ((char)ch) + "'"); 行。如果+、*、? 悬空并且不是前面标记的一部分,则会在此处引发错误。如您所见,{ 不属于引发错误的情况。其实sequence()的case列表中并不存在,编译过程会通过defaultcase直接到atom()。
@SuppressWarnings("fallthrough")
/**
* Parse and add a new Single or Slice.
*/
private Node atom() {
int first = 0;
int prev = -1;
boolean hasSupplementary = false;
int ch = peek();
for (;;) {
switch (ch) {
case '*':
case '+':
case '?':
case '{':
if (first > 1) {
cursor = prev; // Unwind one character
first--;
}
break;
// Irrelevant cases omitted
// [...]
}
break;
}
if (first == 1) {
return newSingle(buffer[0]);
} else {
return newSlice(buffer, first, hasSupplementary);
}
}
当进程进入atom()时,由于马上遇到{,所以从switch和for循环中中断,一个长度为0的新切片被创建(长度来自first,为0)。
当这个切片返回时,量词被closure()解析,得到我们所看到的。
比较Java 1.4.0、Java 5和Java 8的源代码,sequence()和atom()的源代码似乎没有太大变化。这个bug好像从一开始就存在。
正则表达式标准
top-voted answer 引用 IEEE-Standard 1003.1(或 POSIX 标准)与讨论无关,因为 Java 不实现 BRE 和 ERE。
根据标准,有许多语法会导致未定义的行为,但在许多其他正则表达式风格中都是明确定义的行为(尽管它们是否同意是另一回事)。例如,\d 根据标准是未定义的,但它匹配许多正则表达式风格的数字(ASCII/Unicode)。
遗憾的是,正则表达式语法没有其他标准。
然而,Unicode 正则表达式有一个标准,它侧重于 Unicode 正则表达式引擎应具备的功能。 Java Pattern 类或多或少地实现了 1 级支持,如 UTS #18: Unicode Regular Expression 和 RL2.1 中所述(尽管有很多错误)。