Overview

edit

Meziantou.Framework.Language.Regex parses a regular expression pattern into a tree that keeps every character, including the whitespace and comments of extended mode. Use it to analyze a pattern, show its structure in an editor, report its mistakes, or rename one of its groups.

It is a parser, not an engine: nothing in it matches text. Use System.Text.RegularExpressions for that.

dotnet package add Meziantou.Framework.Language.Regex
PageCovers
Dialects.NET, JavaScript, PCRE and POSIX patterns, and what each one supports
Options and capturesThe options that change how a pattern is read, extended mode, and the numbering of groups
Editing patternsReplacing nodes, building patterns, rewriting, and comparing patterns

Parse a pattern

RegexSyntaxTree.ParseText takes the pattern and its dialect. Invalid patterns give diagnostics, not exceptions:

const string Pattern = """
    (?x)                                   # free-spacing mode
    ^
    (?<ip> \d{1,3} (?: \. \d{1,3} ){3} )   # client address
    \s+ (?<status> [1-5]\d{2} )
    $
    """;

var tree = RegexSyntaxTree.ParseText(Pattern, RegexDialect.Net);

// Nothing is lost: the tree reproduces the input character for character.
Assert.Equal(Pattern, tree.GetRoot().ToFullString());
Assert.Empty(tree.GetDiagnostics());

// Invalid input produces diagnostics instead of exceptions.
var invalid = RegexSyntaxTree.ParseText("(a", RegexDialect.Net);
var diagnostic = Assert.Single(invalid.GetDiagnostics());
Assert.Equal("REGEX0002 at (0,0)-(0,2): Unterminated group: expected ')'.", $"{diagnostic.Id} at {diagnostic.Location}: {diagnostic.Message}");

To read a JavaScript regular expression literal, with its slashes and its flags, use ParseJavaScriptLiteral. The flags become the options of the pattern:

var tree = RegexSyntaxTree.ParseJavaScriptLiteral("/a+b/giu");

Assert.Equal("giu", tree.GetRoot().FlagsToken.Text);
Assert.Equal(RegexPatternOptions.IgnoreCase | RegexPatternOptions.Unicode | RegexPatternOptions.Global, tree.PatternOptions);
Assert.Equal("/a+b/giu", tree.GetRoot().ToFullString());

Find the parts of a pattern

A pattern has one root, a RegexPatternSyntax. Reach its groups, classes and atoms through the tree:

var tree = RegexSyntaxTree.ParseText(@"(?<year>\d{4})-[01]\d-[0-3]\d", RegexDialect.Net);

var classes = tree.GetRoot().DescendantNodes().OfType<RegexCharacterClassSyntax>();
var groups = tree.GetRoot().DescendantNodes().OfType<RegexGroupSyntax>();

Assert.Equal(["[01]", "[0-3]"], classes.Select(node => node.ToString()));
Assert.Equal([@"(?<year>\d{4})"], groups.Select(node => node.ToString()));

The body of a pattern is always an alternation of sequences, even with a single branch and no |, so you never handle two shapes of the same thing. Alternation.Branches is a separated list: the branches and the | between them share one sequence.

var tree = RegexSyntaxTree.ParseText("ab|c", RegexDialect.Net);
var branches = tree.GetRoot().Alternation.Branches;

Assert.Equal(2, branches.Count);
Assert.Equal(1, branches.SeparatorCount);
Assert.Equal(2, branches[0].Terms.Count);
Assert.Equal("|", branches.GetSeparator(0).ToString());

Every dialect parses its own syntax into its own node types: a group is a RegexGroupSyntax, a named group a RegexNamedGroupSyntax, a lookaround a RegexLookaroundSyntax, a quantified atom a RegexQuantifiedSyntax, and so on.

A literal is one atom per UTF-16 code unit, and a quantifier applies to the atom in front of it, as the engine reads it: in 😀*, the quantifier applies to the low surrogate alone. With the JavaScript u or v flag, a pattern is a sequence of code points instead: the pair is one atom, and RegexLiteralSyntax.CodePoint gives it.

Diagnostics

The identifiers are grouped by what they are about:

IdsAbout
REGEX0001 to REGEX0019Brackets, parentheses, quantifiers, escapes and group names, in every dialect
REGEX0030 to REGEX0035Backreferences, Unicode properties and duplicate group names
REGEX0050 to REGEX0056Constructs only .NET has, such as class subtraction and conditionals
REGEX0070 to REGEX0075JavaScript: class set operations, class strings and modifiers
REGEX0090 to REGEX0093PCRE: backtracking verbs, callouts, lookbehinds and conditions
REGEX0110, REGEX0111POSIX: bracket expressions and intervals
REGEX0200 and upThe parser itself: nesting deeper than MaxRecursionDepth, and the flags and delimiters of a JavaScript literal

The message says what was found; test the Id in code. See Diagnostics.

Walk a pattern

RegexSyntaxWalker, RegexSyntaxVisitor, RegexSyntaxVisitor<TResult> and RegexSyntaxRewriter cover the node types of every dialect, so one walker handles any pattern:

private sealed class LiteralCounter : RegexSyntaxWalker
{
    public int Count { get; private set; }

    public override void VisitLiteral(RegexLiteralSyntax node)
    {
        Count++;
        base.VisitLiteral(node);
    }
}
var tree = RegexSyntaxTree.ParseText("ab|c", RegexDialect.Net);
var counter = new LiteralCounter();

counter.Visit(tree.GetRoot());

Assert.Equal(3, counter.Count);