# Regex

> Match, replace, split and CAPTURE with regular expressions — the C#-faithful System.Text.RegularExpressions spelling. `Regex.IsMatch(s, "\\d+")` tests a pattern, `Regex.Replace` rewrites every match, `Regex.Split` breaks a string on a pattern, and `Regex.Match`/`Regex.Matches` return match objects whose groups are read by number or by name. Pure — no capability needed. Every call runs under a platform match-timeout.

<!-- id: stdlib-regex · area: stdlib · stability: stable · html: https://osysharp.com/reference/stdlib/regex/ -->

## Summary        {#summary}
**`Regex`** is the regular-expression surface, spelled exactly as in C# (`System.Text.RegularExpressions`):

```osy syntax
var ok    = Regex.IsMatch("order-1042", "\\d+");          // true — the pattern matches somewhere
var clean = Regex.Replace("a1b2c", "\\d", "-");           // "a-b-c" — every match rewritten
var parts = Regex.Split("a, b ,c", "\\s*,\\s*");          // ["a", "b", "c"]
var m     = Regex.Match("ERROR:42", "(?<level>\\w+):(\\d+)");
//          m.Success · m.Value · m.Index · m.Groups["level"] · m.Groups[2]
```

It is **pure** — no capability, no `using` required (though a pasted `using System.Text.RegularExpressions;` is
accepted and does nothing). Patterns are standard .NET regex syntax.

## Signature      {#signature}
```osy syntax
bool         Regex.IsMatch(string input, string pattern)                       // matches anywhere?
string       Regex.Replace(string input, string pattern, string replacement)   // rewrite every match
List<string> Regex.Split(string input, string pattern)                         // split on the pattern
Match        Regex.Match(string input, string pattern)                         // the FIRST match
Match[]      Regex.Matches(string input, string pattern)                       // every match
```

## Description    {#description}
**`IsMatch`** returns true when `pattern` matches anywhere in `input`. Anchor with `^`…`$` for a whole-string match.

**`Replace`** returns `input` with every match rewritten to `replacement`. The replacement supports numbered group
backreferences — `$1`, `$2`, … — exactly as in C#:

```osy title="rewriting with numbered backreferences" syntax
Regex.Replace("2026-07-12", "(\\d+)-(\\d+)-(\\d+)", "$1/$2/$3")   // "2026/07/12"
```

**`Split`** returns the substrings between matches as a `List<string>` you can `foreach`, index, and read `.Count` on.

**Match timeout (host protection).** Every `Regex` call runs under a fixed platform match-timeout. A pattern with
*catastrophic backtracking* (a nested quantifier like `(a+)+$` on a long non-matching input) can otherwise spin a CPU
effectively forever; past the cap the call is aborted rather than allowed to hang. You cannot raise the cap — write a
cheaper pattern if you hit it. This is the only safety limit on the surface; normal patterns never come near it.

### Capturing groups — `Match` and `Matches`   {#groups}

**`Match`** returns the FIRST match; **`Matches`** returns every one, left to right and non-overlapping.

`Match` **always returns an object.** An unsuccessful search answers a `Match` with `Success = false` rather than
null — so `if (m.Success)` is the question you ask, and there is nothing to null-check first.

```osy title="an unsuccessful Match is still an object, not null" syntax
var m = Regex.Match(line, "(?<level>\\w+):(\\d+)");
m.Success        // did it match?
m.Value          // the whole matched text
m.Index          // where it starts, in characters
m.Length         // how long it is
m.Groups.Count   // how many groups, counting the whole match at 0
```

A group is read **by number or by name**, and a group that did not participate answers `Success = false` with an
empty `Value` — never a fault, and never a missing entry:

```osy title="reading a group by number or by name — a missing one is not a fault" syntax
m.Groups[0]          // the WHOLE match — group numbering starts at 0, exactly as in C#
m.Groups[2].Value    // a numbered group
m.Groups["level"]    // a named group, from `(?<level>…)`
m.Groups[99]         // Success = false — out of range is a group that did not participate
m.Groups["nope"]     // Success = false — so an OPTIONAL capture reads without a prior check
```

⚠ **Named groups are numbered AFTER unnamed ones**, which is .NET's rule and not source order. In
`(?<level>\\w+):(\\d+)` the `level` group is number **2** and `(\\d+)` is number **1**. Read a named group by its
name and the question does not arise.

⚠ **`Match` and `Matches` do not push down into a query.** Postgres' regex dialect is POSIX rather than .NET's, and
`regexp_matches` returns a set of arrays rather than a scalar — so a pushed-down match would be a different function
wearing the same name, and the compiler refuses it where you wrote it. Narrow the query with `Regex.IsMatch`, which
DOES push down, and capture from the rows it returns.

⚠ **They run on the SERVER only.** `IsMatch`, `Replace` and `Split` also run in the browser; `Match`/`Matches` do
not yet, and a render expression that calls one is a compile error naming the component. Call them in a function or
an action and render what they produce.

**Still not available.** The compiled-instance form (`new Regex(pattern)`) — the three statics plus the two match
members cover validation, rewriting, tokenizing and extraction.

## Examples       {#examples}

Validate an email field:

```osy title="validate a whole string — anchor it, or it matches anywhere" test app=stdlib-regex
bool LooksLikeEmail(string s) {
  return Regex.IsMatch(s, "^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
}
```

Pull the parts out of a log line — by name, so the group NUMBERS never have to be counted:

```osy title="capture by NAME, so group numbers never have to be counted" test app=stdlib-regex-capture
string Level(string line) {
  var m = Regex.Match(line, "(?<level>[A-Za-z]+):(?<code>[0-9]+)");
  return m.Success ? m.Groups["level"].Value : "";
}

/// Every code in a line, in order — `Matches` is an ordinary array, so `foreach` reads it.
string[] Codes(string line) {
  var out = new List<string>();
  foreach (var m in Regex.Matches(line, "(?<level>[A-Za-z]+):(?<code>[0-9]+)")) {
    out.Add(m.Groups["code"].Value);
  }
  return out.ToArray();
}
```

Normalise whitespace and strip punctuation:

```osy title="rewrite every match — punctuation into a slug" test app=stdlib-regex
string Slugify(string title) {
  var lower = Text.Lower(Text.Trim(title));
  var spaced = Regex.Replace(lower, "[^a-z0-9]+", "-");   // non-alphanumerics → a single dash
  return spaced;
}
```

Tokenise a delimited line, tolerating irregular spacing:

```osy title="split on a pattern, tolerating irregular spacing" test app=stdlib-regex
List<string> Fields(string csvLine) {
  return Regex.Split(csvLine, "\\s*,\\s*");               // "a, b ,c" → ["a", "b", "c"]
}
```

## See also       {#see-also}
- <span class="planned" title="this page is planned and not written yet">stdlib-text</span> — the plain-string operations (`Upper`, `Contains`, `Replace`, `Split`) for non-pattern work
- [JsonSerializer](https://osysharp.com/reference/json/serializer/) — the other pure BCL surface for parsing structured text
