christiangeorgelucas/regex-tools
v0.1.1GoThe regex-tools package provides a set of composable regular-expression nodes that leverage Go's standard library `regexp` package, which is based on Google's RE2 engine. It allows users to efficiently test patterns, find matches, extract named groups, replace matches, split text, and validate regex patterns while ensuring linear-time performance and avoiding catastrophic backtracking.
Published by Christian George Lucas· MIT
Use cases
- •Validate user input formats like email addresses
- •Extract structured data from log files
- •Perform bulk text replacements in documents
- •Split CSV data into manageable pieces
- •Lint and validate regex patterns before deployment
Nodes (8)
The FindAll node identifies every non-overlapping match of a specified RE2 pattern within the input text, returning details such as the full text, byte offsets, and numbered capture groups for each match. It supports a limit on the number of matches returned and indicates if there are additional matches beyond the specified limit.
View source
package nodes
import (
"context"
"christiangeorgelucas/regex-tools/axiom"
gen "christiangeorgelucas/regex-tools/gen"
)
// FindAll returns every non-overlapping match of an RE2 pattern in input,
// leftmost-first, each with its full text, byte offsets, and numbered
// capture groups. limit caps how many matches come back (default 1000,
// hard-capped at 10000); truncated reports whether the cap was hit.
func FindAll(ctx context.Context, ax axiom.Context, input *gen.RegexFindAllRequest) (*gen.RegexFindAllResult, error) {
re, code := compilePattern(input.GetPattern(), input.GetFlags())
if code != "" {
return &gen.RegexFindAllResult{Error: code}, nil
}
limit := clampFindLimit(input.GetLimit())
// Ask for one more than the cap so we can tell whether there were more
// matches beyond it (truncated) without a second, unbounded pass.
all := re.FindAllStringSubmatchIndex(input.GetInput(), limit+1)
truncated := len(all) > limit
if truncated {
all = all[:limit]
}
matches := make([]*gen.RegexMatch, 0, len(all))
for _, idx := range all {
matches = append(matches, matchFromIndex(input.GetInput(), idx))
}
return &gen.RegexFindAllResult{
Matches: matches,
Count: int32(len(matches)),
Truncated: truncated,
}, nil
}
The FindFirst node locates the leftmost match of a specified RE2 pattern within the input text, returning details about the match including the matched substring, its byte offsets, and any capture groups. If no match is found, it returns a structured result indicating the absence of a match.
View source
package nodes
import (
"context"
"christiangeorgelucas/regex-tools/axiom"
gen "christiangeorgelucas/regex-tools/gen"
)
// FindFirst returns the leftmost match of an RE2 pattern in input, with the
// full matched text, its byte offsets, and every numbered capture group's
// text and offsets. found is false when the pattern does not match at all.
func FindFirst(ctx context.Context, ax axiom.Context, input *gen.RegexFindRequest) (*gen.RegexFindResult, error) {
re, code := compilePattern(input.GetPattern(), input.GetFlags())
if code != "" {
return &gen.RegexFindResult{Error: code}, nil
}
idx := re.FindStringSubmatchIndex(input.GetInput())
if idx == nil {
return &gen.RegexFindResult{Found: false}, nil
}
return &gen.RegexFindResult{
Found: true,
Match: matchFromIndex(input.GetInput(), idx),
}, nil
}
The FindNamedGroups node extracts named capture groups from regex matches, returning them as a name-value map for each match. It omits any named groups that did not participate in a match, allowing for clear distinction between matched and unmatched groups.
View source
package nodes
import (
"context"
"christiangeorgelucas/regex-tools/axiom"
gen "christiangeorgelucas/regex-tools/gen"
)
// FindNamedGroups extracts every match's named capture groups as a direct
// name→value map, e.g. pattern `(?P<year>\d{4})-(?P<month>\d{2})` on
// "2026-07" yields {"year":"2026","month":"07"}. A named group that did not
// participate in a given match is omitted from that match's map. limit and
// truncation behave exactly as FindAll's.
func FindNamedGroups(ctx context.Context, ax axiom.Context, input *gen.RegexNamedGroupsRequest) (*gen.RegexNamedGroupsResult, error) {
re, code := compilePattern(input.GetPattern(), input.GetFlags())
if code != "" {
return &gen.RegexNamedGroupsResult{Error: code}, nil
}
names := re.SubexpNames() // names[0] is always "" (whole match)
namesPresent := false
for _, n := range names {
if n != "" {
namesPresent = true
break
}
}
limit := clampFindLimit(input.GetLimit())
all := re.FindAllStringSubmatchIndex(input.GetInput(), limit+1)
truncated := len(all) > limit
if truncated {
all = all[:limit]
}
text := input.GetInput()
results := make([]*gen.RegexNamedGroupMatch, 0, len(all))
for _, idx := range all {
groups := make(map[string]string)
for i := 1; i < len(names); i++ {
if names[i] == "" {
continue
}
gs, ge := idx[2*i], idx[2*i+1]
if gs < 0 {
continue // group did not participate in this match
}
groups[names[i]] = text[gs:ge]
}
results = append(results, &gen.RegexNamedGroupMatch{Groups: groups})
}
return &gen.RegexNamedGroupsResult{
Matches: results,
Count: int32(len(results)),
Truncated: truncated,
GroupNamesPresent: namesPresent,
}, nil
}
The Replace node substitutes the first match of a specified RE2 pattern in the input text with a replacement string, allowing for the use of capture groups. If no match is found, the output remains unchanged and an error is returned for malformed input.
View source
package nodes
import (
"context"
"christiangeorgelucas/regex-tools/axiom"
gen "christiangeorgelucas/regex-tools/gen"
)
// Replace substitutes only the first (leftmost) match of an RE2 pattern in
// input with replacement, which may reference capture groups via Go's
// expand syntax ($1, ${1}, ${name}; $$ for a literal dollar). replaced is
// false (output equals input unchanged) when the pattern does not match.
func Replace(ctx context.Context, ax axiom.Context, input *gen.RegexReplaceRequest) (*gen.RegexReplaceResult, error) {
re, code := compilePattern(input.GetPattern(), input.GetFlags())
if code != "" {
return &gen.RegexReplaceResult{Error: code}, nil
}
text := input.GetInput()
idx := re.FindStringSubmatchIndex(text)
if idx == nil {
return &gen.RegexReplaceResult{Output: text, Replaced: false}, nil
}
expanded := re.ExpandString(nil, input.GetReplacement(), text, idx)
output := text[:idx[0]] + string(expanded) + text[idx[1]:]
return &gen.RegexReplaceResult{Output: output, Replaced: true}, nil
}
The ReplaceAll node replaces every non-overlapping match of an RE2 pattern in the input text with a specified replacement, returning the modified text and the count of substitutions made. It utilizes Go's regexp.ReplaceAllString functionality, ensuring efficient performance and proper backreference handling.
View source
package nodes
import (
"context"
"strings"
"christiangeorgelucas/regex-tools/axiom"
gen "christiangeorgelucas/regex-tools/gen"
)
// ReplaceAll substitutes every non-overlapping match of an RE2 pattern in
// input with replacement (same $1/${name} backreference syntax as Replace),
// returning the resulting text and how many substitutions were made. RE2's
// linear-time guarantee bounds the match search itself; a large result (a
// zero-width match repeated many times with a long replacement) is the
// platform's payload/memory concern, not this node's, so it's returned in
// full rather than truncated.
func ReplaceAll(ctx context.Context, ax axiom.Context, input *gen.RegexReplaceAllRequest) (*gen.RegexReplaceAllResult, error) {
re, code := compilePattern(input.GetPattern(), input.GetFlags())
if code != "" {
return &gen.RegexReplaceAllResult{Error: code}, nil
}
text := input.GetInput()
allIdx := re.FindAllStringSubmatchIndex(text, -1)
if len(allIdx) == 0 {
return &gen.RegexReplaceAllResult{Output: text, Replacements: 0}, nil
}
var b strings.Builder
b.Grow(len(text))
last := 0
for _, idx := range allIdx {
b.WriteString(text[last:idx[0]])
b.Write(re.ExpandString(nil, input.GetReplacement(), text, idx))
last = idx[1]
}
b.WriteString(text[last:])
return &gen.RegexReplaceAllResult{
Output: b.String(),
Replacements: int32(len(allIdx)),
}, nil
}
The Split node processes input text by dividing it into segments based on matches of a specified RE2 regular expression pattern, returning the resulting pieces in order. It allows for a limit on the number of pieces returned, with any excess being combined into the last segment.
View source
package nodes
import (
"context"
"christiangeorgelucas/regex-tools/axiom"
gen "christiangeorgelucas/regex-tools/gen"
)
// Split slices input on every match of an RE2 pattern, returning the pieces
// strictly between (and around) matches, in order. A positive limit caps
// the result at that many pieces, Go regexp.Split-style, with the
// remainder folded into the last piece; limit <= 0 (including an omitted
// field, since proto3 has no way to distinguish "not set" from "explicitly
// zero") means "every piece" rather than Go's own literal n==0 "no pieces"
// behavior — a default of "return nothing" would be a footgun for any
// caller that simply omits limit. A large piece count for an unbounded
// request is the platform's response-size concern, not this node's.
func Split(ctx context.Context, ax axiom.Context, input *gen.RegexSplitRequest) (*gen.RegexSplitResult, error) {
re, code := compilePattern(input.GetPattern(), input.GetFlags())
if code != "" {
return &gen.RegexSplitResult{Error: code}, nil
}
text := input.GetInput()
requested := int(input.GetLimit())
n := -1 // "every piece" (Go's regexp.Split unbounded sentinel)
if requested > 0 {
n = requested
}
parts := re.Split(text, n)
return &gen.RegexSplitResult{
Parts: parts,
Count: int32(len(parts)),
}, nil
}
The Test node evaluates whether a given RE2 regular expression pattern matches any part of the input text, providing a simple yes/no answer. It utilizes Go's efficient linear-time RE2 engine to prevent performance issues from adversarial patterns and returns structured error messages for invalid patterns.
View source
package nodes
import (
"context"
"christiangeorgelucas/regex-tools/axiom"
gen "christiangeorgelucas/regex-tools/gen"
)
// Test reports whether an RE2 pattern matches anywhere in input, using Go's
// linear-time regexp engine — so an adversarial pattern cannot cause
// catastrophic-backtracking ReDoS. Pure function; safe to call repeatedly.
func Test(ctx context.Context, ax axiom.Context, input *gen.RegexTestRequest) (*gen.RegexTestResult, error) {
re, code := compilePattern(input.GetPattern(), input.GetFlags())
if code != "" {
return &gen.RegexTestResult{Error: code}, nil
}
return &gen.RegexTestResult{Matched: re.MatchString(input.GetInput())}, nil
}
The ValidatePattern node checks the validity of an RE2 regular expression pattern by compiling it without matching against any input. It reports any syntax errors and, if valid, provides the count of capture groups and their names.
View source
package nodes
import (
"context"
"fmt"
"regexp"
"strings"
"christiangeorgelucas/regex-tools/axiom"
gen "christiangeorgelucas/regex-tools/gen"
)
// ValidatePattern compile-checks an RE2 pattern without matching it against
// any input. When invalid, error_message holds Go regexp/syntax's own
// human-readable compile error (naming the offending construct — e.g. an
// unsupported PCRE-only backreference or lookaround RE2 deliberately does
// not support). When valid, group_count and group_names report the
// pattern's numbered capture groups in source order.
func ValidatePattern(ctx context.Context, ax axiom.Context, input *gen.RegexValidateRequest) (*gen.RegexValidateResult, error) {
pattern := input.GetPattern()
flags := input.GetFlags()
if pattern == "" {
return &gen.RegexValidateResult{Valid: false, ErrorMessage: "pattern must not be empty"}, nil
}
for _, c := range flags {
if !strings.ContainsRune(validFlagChars, c) {
return &gen.RegexValidateResult{
Valid: false,
ErrorMessage: fmt.Sprintf("invalid flag %q: only i, m, s, U are supported", string(c)),
}, nil
}
}
full := pattern
if flags != "" {
full = "(?" + flags + ")" + pattern
}
re, err := regexp.Compile(full)
if err != nil {
return &gen.RegexValidateResult{Valid: false, ErrorMessage: err.Error()}, nil
}
names := re.SubexpNames() // names[0] is always "" (whole match)
groupNames := make([]string, 0, len(names)-1)
if len(names) > 1 {
groupNames = append(groupNames, names[1:]...)
}
return &gen.RegexValidateResult{
Valid: true,
GroupCount: int32(len(groupNames)),
GroupNames: groupNames,
}, nil
}
Messages (19)
Download .protoThe regular expression pattern to search for in the input text.
The text in which to search for matches of the specified pattern.
Flags that modify the behavior of the regex engine, such as case sensitivity.
The maximum number of matches to return; defaults to 1000 if omitted or less than or equal to 0.
An array of found matches, each containing the full text and byte offsets.
The total number of matches returned in the response.
A boolean indicating whether the number of matches exceeded the specified limit.
The regular expression pattern to search for in the input text.
The text in which to search for the specified pattern.
Flags that modify the behavior of the regex engine, such as case sensitivity.
A boolean indicating whether a match was found.
An object containing details about the matched substring and capture groups, if a match was found.
A string containing any error message if the pattern compilation fails.
No fields
The regex pattern containing named capture groups to be matched against the input string.
The input string on which the regex pattern will be applied.
Optional flags that modify the behavior of the regex matching process.
The maximum number of matches to return; if exceeded, results will be truncated.
An array of maps where each map contains the named groups and their corresponding values for each match.
The total number of matches found, which may be less than the limit if truncation occurred.
A boolean indicating whether the results were truncated due to exceeding the specified limit.
A boolean indicating whether any named groups were declared in the regex pattern.
The regular expression pattern to match against the input text.
The original text in which the replacements will be made.
The string to replace each match with, supporting backreference syntax.
Flags that modify the behavior of the regex matching, such as case sensitivity.
The resulting text after all replacements have been made.
The total number of substitutions that were performed.
The regular expression pattern to search for in the input text.
The text in which the regex pattern will be searched and replaced.
The string that will replace the matched pattern, which can reference capture groups.
Flags that modify the behavior of the regex matching, such as case sensitivity.
The resulting text after the replacement has been applied.
A boolean indicating whether a replacement was made (true) or if the input text remains unchanged (false).
A structured error message if the input is malformed or if there is an issue with the regex pattern.
The regular expression pattern used to determine where to split the input text.
The text string that will be split according to the specified pattern.
Flags that modify the behavior of the regular expression matching.
An integer that specifies the maximum number of pieces to return; a value of 0 or less indicates that all pieces should be returned.
The regular expression pattern to be tested against the input text.
The text input in which the regular expression pattern is searched for a match.
Optional flags that modify the behavior of the regular expression matching.
A boolean indicating whether the pattern matched the input text.
A structured error message indicating an invalid pattern if applicable.
The regular expression pattern to be validated.
Optional flags that modify the behavior of the regex, such as case sensitivity.
Indicates whether the provided pattern is valid.
Contains a human-readable error message if the pattern is invalid.
The number of capture groups in the valid pattern.
An array of names for each capture group in the pattern, with empty strings for unnamed groups.
Use as a tool in any AI agent
christiangeorgelucas/regex-tools is callable as a tool from any MCP client (Claude, Cursor, or your own agent) via Axiom's hosted MCP server — no install, no download. Add the server, then your agent can search and invoke it, or pin it as a typed tool.
claude mcp add --transport http axiom https://api.axiomide.com/mcp --header "Authorization: Bearer YOUR_AXIOM_API_KEY"Replace YOUR_AXIOM_API_KEY with a key you create in Console → API Keys.
MCP server setup & other clients →Use this package
Sign in to import christiangeorgelucas/regex-tools into the graph editor — drop it into a flow and hit run. Or call it directly via the API Reference with your API key.
Sign in to useOpen editor