phplrt 4.0

Regular Expressions

A grammar that reads PCRE - the notation the %token lines of every other example here are written in:

1(?<year>[0-9]{4})-(?P<month>[0-9]{2})-(?:[0-9]{2})
2(?i)^\Afoo(?=bar)(?!baz)(?<=qux)end\z$
3a?b??c?+d*e*?f*+g+h+?i++j{2}k{2,5}l{2,5}?m{2,}+

Almost all of the work happens in the lexer, and the parser that follows is three rules long: an alternation of concatenations of quantified things.

That split is the lesson. A character class is one token - \[\^?\]?(?:\\.|\[:\^?[a-z]++:\]|[^\]])*+\] - because inside [...] every character stands for itself, * and | included; letting the parser see the pieces would mean teaching it to un-see them. The same goes for (?#...), (?<name> and (?(1), each of which ends at a character of its own and is read whole.

And where two spellings begin alike, the longer one is declared first: \?\+ before \?\? before \?, and the \(\?<= of a lookbehind before the \(\?P?< of a named group. The lexer takes the first pattern that matches, not the longest, so the order is the rule.

Grammar

  1/**
  2 * -----------------------------------------------------------------------------
  3 *  PCRE
  4 * -----------------------------------------------------------------------------
  5 *
  6 * The pattern a Perl-compatible regular expression is written of, without the
  7 * delimiters and the modifiers around it.
  8 *
  9 * @see https://github.com/hoaproject/Regex/blob/master/Source/Grammar.pp
 10 */
 11
 12%pragma root Expression
 13
 14/**
 15 * -----------------------------------------------------------------------------
 16 *  Groups
 17 * -----------------------------------------------------------------------------
 18 *
 19 *  A group is told from another by what is written past its parenthesis, so
 20 *  the longer spelling is declared before the shorter one it begins with.
 21 *
 22 *  A comment, a name and a reference each end at a character of their own, so
 23 *  each is read whole rather than by switching the reading over to a lexer of
 24 *  its own.
 25 */
 26
 27%token T_INTERNAL_OPTION        \(\?[\-+]?[imsxUXn]++\)
 28
 29%token T_COMMENT_GROUP          \(\?#((?:\\.|[^)])*+)\)
 30
 31%token T_LOOKAHEAD              \(\?=
 32%token T_NEGATIVE_LOOKAHEAD     \(\?!
 33%token T_LOOKBEHIND             \(\?<=
 34%token T_NEGATIVE_LOOKBEHIND    \(\?<!
 35
 36%token T_NAMED_REFERENCE        \(\?\(<((?:\\.|[^>])++)>\)
 37%token T_RELATIVE_REFERENCE     \(\?\(([+\-][0-9]++)\)
 38%token T_ABSOLUTE_REFERENCE     \(\?\(([0-9]++)\)
 39// The assertion a condition is written on opens with a parenthesis of its own
 40%token T_ASSERTION_REFERENCE    \(\?(?=\()
 41
 42%token T_NAMED_CAPTURING        \(\?P?<((?:\\.|[^>])++)>
 43%token T_NON_CAPTURING          \(\?:
 44%token T_NON_CAPTURING_RESET    \(\?\|
 45%token T_ATOMIC_GROUP           \(\?>
 46%token T_CAPTURING_OPEN         \(
 47%token T_CAPTURING_CLOSE        \)
 48
 49/**
 50 * -----------------------------------------------------------------------------
 51 *  Character Class
 52 * -----------------------------------------------------------------------------
 53 *
 54 *  Every character of a class stands for itself, an operator and a quantifier
 55 *  included, so a class is read whole rather than by the tokens it is written
 56 *  of. A bracket written first closes nothing, and a backslash escapes the
 57 *  character past it.
 58 */
 59
 60%token T_CHARACTER_CLASS        \[\^?\]?(?:\\.|\[:\^?[a-z]++:\]|[^\]])*+\]
 61
 62/**
 63 * -----------------------------------------------------------------------------
 64 *  Quantifiers
 65 * -----------------------------------------------------------------------------
 66 */
 67
 68%token T_ZERO_OR_ONE_POSSESSIVE   \?\+
 69%token T_ZERO_OR_ONE_LAZY         \?\?
 70%token T_ZERO_OR_ONE              \?
 71%token T_ZERO_OR_MORE_POSSESSIVE  \*\+
 72%token T_ZERO_OR_MORE_LAZY        \*\?
 73%token T_ZERO_OR_MORE             \*
 74%token T_ONE_OR_MORE_POSSESSIVE   \+\+
 75%token T_ONE_OR_MORE_LAZY         \+\?
 76%token T_ONE_OR_MORE              \+
 77%token T_EXACTLY_N                \{[0-9]++\}
 78%token T_N_TO_M_POSSESSIVE        \{[0-9]++,[0-9]++\}\+
 79%token T_N_TO_M_LAZY              \{[0-9]++,[0-9]++\}\?
 80%token T_N_TO_M                   \{[0-9]++,[0-9]++\}
 81%token T_N_OR_MORE_POSSESSIVE     \{[0-9]++,\}\+
 82%token T_N_OR_MORE_LAZY           \{[0-9]++,\}\?
 83%token T_N_OR_MORE                \{[0-9]++,\}
 84
 85/**
 86 * -----------------------------------------------------------------------------
 87 *  Literals
 88 * -----------------------------------------------------------------------------
 89 *
 90 *  The last of them reads any character at all, so it is declared last.
 91 */
 92
 93%token T_ALTERNATION            \|
 94%token T_CHARACTER              \\(?:[aefnrt]|c[\x00-\x7f])
 95%token T_DYNAMIC_CHARACTER      \\(?:[0-7]{3}|x[0-9a-fA-F]{2}|x\{[0-9a-fA-F]++\})
 96%token T_CHARACTER_TYPE         \\(?:[CdDhHNRsSvVwWX]|[pP]\{[^}]++\})
 97%token T_ANCHOR                 \\[bBAZzG]|\^|\$
 98%token T_MATCH_POINT_RESET      \\K
 99// A bracket opens a class and never stands for itself, so it is written "\["
100%token T_LITERAL                \\.|[^\[]
101
102/**
103 * -----------------------------------------------------------------------------
104 *  Expression
105 * -----------------------------------------------------------------------------
106 */
107
108Expression
109  : Alternation()
110  ;
111
112Alternation
113  : Concatenation() (::T_ALTERNATION:: Concatenation())*
114  ;
115
116Concatenation
117  : (InternalOptions() | Assertion() | Condition() | Quantification())+
118  ;
119
120// (?i), (?-x)
121InternalOptions
122  : <T_INTERNAL_OPTION>
123  ;
124
125// (?=...), (?<!...)
126Assertion
127  : ( <T_LOOKAHEAD>
128    | <T_NEGATIVE_LOOKAHEAD>
129    | <T_LOOKBEHIND>
130    | <T_NEGATIVE_LOOKBEHIND>
131    )
132    Alternation() ::T_CAPTURING_CLOSE::
133  ;
134
135// (?(1)yes|no), (?(<name>)yes|no), (?(?=...)yes|no)
136Condition
137  : ( <T_NAMED_REFERENCE>
138    | <T_RELATIVE_REFERENCE>
139    | <T_ABSOLUTE_REFERENCE>
140    | ::T_ASSERTION_REFERENCE:: Assertion()
141    )
142    Concatenation()?
143    (::T_ALTERNATION:: Concatenation()?)?
144    ::T_CAPTURING_CLOSE::
145  ;
146
147Quantification
148  : (CharacterClass() | Simple()) Quantifier()?
149  ;
150
151Quantifier
152  : <T_ZERO_OR_ONE_POSSESSIVE>
153  | <T_ZERO_OR_ONE_LAZY>
154  | <T_ZERO_OR_ONE>
155  | <T_ZERO_OR_MORE_POSSESSIVE>
156  | <T_ZERO_OR_MORE_LAZY>
157  | <T_ZERO_OR_MORE>
158  | <T_ONE_OR_MORE_POSSESSIVE>
159  | <T_ONE_OR_MORE_LAZY>
160  | <T_ONE_OR_MORE>
161  | <T_EXACTLY_N>
162  | <T_N_TO_M_POSSESSIVE>
163  | <T_N_TO_M_LAZY>
164  | <T_N_TO_M>
165  | <T_N_OR_MORE_POSSESSIVE>
166  | <T_N_OR_MORE_LAZY>
167  | <T_N_OR_MORE>
168  ;
169
170// [a-z], [^0-9], [[:alpha:]]
171CharacterClass
172  : <T_CHARACTER_CLASS>
173  ;
174
175Simple
176  : Group()
177  | Literal()
178  ;
179
180Group
181  : <T_COMMENT_GROUP>
182  | ( <T_NAMED_CAPTURING>
183    | <T_NON_CAPTURING>
184    | <T_NON_CAPTURING_RESET>
185    | <T_ATOMIC_GROUP>
186    | ::T_CAPTURING_OPEN::
187    )
188    Alternation() ::T_CAPTURING_CLOSE::
189  ;
190
191Literal
192  : <T_CHARACTER>
193  | <T_DYNAMIC_CHARACTER>
194  | <T_CHARACTER_TYPE>
195  | <T_ANCHOR>
196  | <T_MATCH_POINT_RESET>
197  | <T_LITERAL>
198  ;

Usage

1use Phplrt\Compiler\Compiler;
2use Phplrt\Source\File;
3use Phplrt\Source\Source;
4
5$parser = new Compiler()
6    ->load(new File(__DIR__ . '/grammar.pp3'))
7    ->getParser();
8
9$pattern = $parser->parse(new Source('(?<year>[0-9]{4})-[0-9]{2}'));

The pattern is read without the delimiters and the modifiers around it, so strip the /.../i before handing it over. What comes back is enough to count the groups in a pattern, to rename them, to explain a regular expression to a human, or to generate a string that matches one.

25+ more grammars. phplrt/grammars collects ready to read grammars for real languages - JSON5, TSV, semantic versions, DQL, PHQL, JMS types, PSR-5 and Doctrine annotations, Symfony expressions, Go! AOP pointcuts, Praspel contracts and more - each with sample inputs and a test that keeps it honest.