Recognizing Text Patterns
Some text formats are more expressive than a fixed prefix but far smaller than a grammar. A record may begin with a literal marker, require two letters and a digit, and allow an arbitrary payload before a known suffix. Writing a parser for that boundary would be excessive, while a chain of manual searches can hide the shape you meant to validate.
StringPattern fills this narrow space.
It combines literals, decoded characters, ranges, and one front/back divider into a reusable recognizer for UTF-8,
UTF-16, or UTF-32 text.
This page shows both the compact pattern language and the typed construction API, then uses the resulting pattern to
match, trim, split, and recover native positions.
Keep the boundary of this tool in mind as you read.
A fixed prefix remains clearer as startsWith(), and a sequential grammar with optional branches, values, quoting, or
rich diagnostics belongs in StringCharReader.
StringPattern is at its best when the complete small shape remains easy to see.
For full-string globs, use StringPattern::fromGlob().
This factory supports multiple stars with the same caret-negated bracket sets and escapes as Divider patterns.
Its typed overload accepts repeated Star elements instead of Divider.
Use StringPattern::fromPosixGlob() for the preserved Python-style bracket syntax, including ! negation and
literal backslashes.
Both glob kinds match the complete candidate, including directory separators.
The parsed and typed fromDivider() overloads below select prefix/divider semantics.
See Text Matching and Splitting for the glob syntax and matching boundaries.
Constructing a StringPattern from Parsed Syntax
Each parsed factory accepts AnyString, including implicit conversions from UTF-8, UTF-16, and UTF-32 strings and
literals.
For the common UTF-8 case, construct a pattern from a ""_el literal:
const auto pattern = el::StringPattern::fromDivider("probe-??[0-9]*"_el);
The factory parses and validates the pattern before returning it.
Invalid syntax throws
ParseError.
This form is well suited for configuration files and for application code where a compact pattern is easier to read than an equivalent sequence of typed elements.
text/StringPattern/PatternConstruction.cpp
/// `StringPattern` parses a compact shell-like pattern once and reuses it for many text checks.
///
/// The parsed syntax is useful for small library-level recognizers: a literal prefix, one decoded character with `?`,
/// bracket character sets, and one `*` divider for front/back matching. Backslash escapes keep special characters
/// literal when they are part of the text format itself.
void patternConstruction() {
// A pattern can describe the small fixed shell around a laboratory record.
const auto probeRecord = el::StringPattern::fromDivider("probe-??[0-9]*"_el);
const auto probeLines = el::StringList{{
"probe-AB7 temperatur=21.4C"_el,
"probe-A7 temperatur=21.4C"_el,
}};
const auto lineFormat = el::StringFormat{"{:30}: {}"};
for (const auto &line : probeLines) {
el::io::printLine(lineFormat.build(line, probeRecord.matches(line)));
}
// Escapes make the pattern syntax available for literal diagnostic markers.
const auto marker = el::StringPattern::fromDivider("messung\\[\\?\\]\\*"_el);
el::io::printLine("escaped marker : "_el, marker.matches("messung[?]* archiviert"_el));
}
Output
probe-AB7 temperatur=21.4C : true
probe-A7 temperatur=21.4C : false
escaped marker : true
Pattern Syntax
A pattern consists of decoded text plus a small set of special characters. It always matches at the boundary of a string.
Without a divider, the pattern matches the prefix. With a divider, the text before the divider matches the prefix and the text after the divider matches the suffix.
The syntax intentionally remains small and predictable.
textLiteral decoded characters.
The pattern
messung:matches the prefix ofmessung:temperatur=21.4.?Exactly one decoded Unicode character.
This matches one character, not one byte. The pattern
probe-??matches bothprobe-A7andprobe-µ7.[a-z]A character set using the same compact range syntax as
CharSet. It matches one decoded character from the set.*The divider.
It does not represent a repeated wildcard. Instead, it separates the prefix and suffix portions of the pattern so both can be validated without scanning the entire string like a regular expression engine.
\?,\*,\[,\],\\Escaped special characters.
Only these escapes are supported. Restricting the syntax keeps mistakes visible instead of silently accepting unsupported constructs.
A pattern may contain only one divider.
Pattern |
Meaning |
Example |
|---|---|---|
|
Match the prefix |
|
|
Match the same prefix with an explicit divider. |
|
|
Match the suffix |
|
|
Match prefix |
|
The pattern * is invalid.
Patterns containing more than one divider, such as a*b*c, are also invalid.
Constructing a StringPattern from Typed Elements
For library-internal patterns, the typed fromDivider() overload avoids the parsed syntax entirely.
It builds immutable compiled pattern data directly, stores literal text as UTF-32, and makes each pattern element
explicit.
text/StringPattern/StaticConstruction.cpp
/// Typed construction builds a `StringPattern` from explicit pattern elements.
///
/// This form skips parsed pattern syntax. Literal text is supplied as UTF-32 through `Text`, and character choices are
/// expressed with `OneChar`, `Range`, and `Set`. `Divider` is the single front/back divider that corresponds to `*` in
/// parsed patterns.
void staticConstruction() {
using namespace el::pattern;
static const auto acceptedRecord = el::StringPattern::fromDivider(
Text{U"probe-"}, Range{U'A', U'Z'}, Set{{U'0', U'9'}}, OneChar{}, Divider{}, Text{U";ok"});
const auto probeLines = el::StringList{{
"probe-A7b wert=0.42;ok"_el,
"probe-a7b wert=0.42;ok"_el,
"probe-A7b wert=0.42;prüfen"_el,
}};
const auto lineFormat = el::StringFormat{"{:30}: {}"};
for (const auto &line : probeLines) {
el::io::printLine(lineFormat.build(line, acceptedRecord.matches(line)));
}
}
Output
probe-A7b wert=0.42;ok : true
probe-a7b wert=0.42;ok : false
probe-A7b wert=0.42;prüfen : false
Pattern Elements
The elements are defined in erbsland::text::pattern.
Most static patterns begin with:
using namespace el::pattern;
TextLiteral UTF-32 text.
The type stores a
std::u32string_view, so literals use theUprefix.OneCharOne decoded character. Equivalent to
?in the parsed syntax.RangeOne inclusive
CharRange.SetOne character set built from several ranges or from an existing
CharSet.The API uses the name
Set. The public header is namedPatternSet.hppto avoid filename collisions with other publicSetheaders.DividerThe prefix/suffix divider. Equivalent to
*in the parsed syntax.
The number of pattern elements and character ranges is intentionally limited. A string pattern is designed to describe a small boundary shape rather than a general-purpose grammar.
Matching String Prefixes and Suffixes
matches() answers the most common question: does this text have the expected shape?
Use it whenever the surrounding code only needs a yes/no decision.
text/StringPattern/Matches.cpp
/// `matches()` answers whether a text has the expected decoded-character shape.
///
/// A pattern without `*` matches the front of the string. A trailing `*` keeps that prefix behavior explicit.
/// A leading `*` checks the end. A pattern with text on both sides of `*` requires both the prefix and suffix without
/// overlap.
void matches() {
const auto nameOnly = el::StringPattern::fromDivider("messung:"_el);
const auto withUnit = el::StringPattern::fromDivider("messung:*;C"_el);
const auto finished = el::StringPattern::fromDivider("*;fertig"_el);
const auto temperature = el::String{"messung:temperatur=21.4;C"_el};
const auto completed = el::String{"messung:leitwert=0.42;fertig"_el};
el::io::printLine("front only ..............: "_el, nameOnly.matches(temperature));
el::io::printLine("front and back ..........: "_el, withUnit.matches(temperature));
el::io::printLine("suffix only .............: "_el, finished.matches(completed));
el::io::printLine("wrong suffix ............: "_el, finished.matches(temperature));
el::io::printLine("missing suffix ..........: "_el, withUnit.matches("messung:temperatur=21.4"_el));
}
Output
front only ..............: true
front and back ..........: true
suffix only .............: true
wrong suffix ............: false
missing suffix ..........: false
The method does not return captures or explain why matching failed.
If you need diagnostics or want to parse several fields after validating the boundary, continue with
StringCharReader or use the
reader directly.
Removing Matching Prefixes and Suffixes
trimmed() returns a view with the matching shell removed.
trim() removes the matching shell from a string editor and reports whether any text was removed.
Read-only strings remain unchanged; assign the result of trimmed() when replacing a read-only string value.
For a prefix pattern, the prefix is removed. For a suffix pattern, the suffix is removed. For a pattern containing both a prefix and a suffix, both are removed and only the middle remains.
This provides a compact way to unwrap small protocol-like envelopes.
text/StringPattern/TrimAndTrimmed.cpp
/// `trimmed()` and `trim()` remove the matched front or back shell of a string pattern.
///
/// For a pattern with text on both sides of the divider, the divider represents the part that stays. This is useful
/// when a protocol-like string wraps the meaningful value in a predictable prefix and suffix.
void trimAndTrimmed() {
const auto envelope = el::StringPattern::fromDivider("messung:*;ok"_el);
const auto original = el::String{"messung:temperatur=21.4C;ok"_el};
el::io::printLine("original ................: "_el, original);
el::io::printLine("trimmed .................: "_el, envelope.trimmed(original));
auto narrowed = el::StringEditor{"messung:ph=7.1;ok"_el};
if (envelope.trim(narrowed)) {
el::io::printLine("narrowed ................: "_el, narrowed);
}
auto unchanged = el::StringEditor{"messung:ph=7.1;prüfen"_el};
el::io::printLine("changed .................: "_el, envelope.trim(unchanged));
el::io::printLine("kept ....................: "_el, unchanged);
}
Output
original ................: messung:temperatur=21.4C;ok
trimmed .................: temperatur=21.4C
narrowed ................: ph=7.1
changed .................: false
kept ....................: messung:ph=7.1;prüfen
Trimming is deliberately conservative.
If the pattern does not match, the original text remains unchanged and trim() returns false.
These methods are intended for removing a single matching shell. To remove repeated occurrences throughout a string, use the normal string editing and search functions instead.
Splitting Text at Pattern Match Positions
split() returns two views.
The first contains the text matched by the prefix portion of the pattern. The second contains everything after that boundary.
This is particularly useful for UTF-8 text.
A pattern such as probe-?? may consume different numbers of bytes depending on the matched Unicode characters.
split() calculates the correct native split position automatically.
/// `split()` returns the matched text and the remaining text at the pattern boundary.
///
/// The method is especially handy when `?` or bracket sets make the matched prefix variable in bytes. For a pattern
/// with both a prefix and suffix, the suffix is validated and then dropped from the second result.
void split() {
const auto recordPattern = el::StringPattern::fromDivider("probe-??*;ok"_el);
const auto record = el::String{"probe-A7 temperatur=21.4C;ok"_el};
const auto [recordId, valueText] = recordPattern.split(record);
el::io::printLine("record id ...............: "_el, recordId);
el::io::printLine("value ...................: "_el, valueText);
const auto suffixPattern = el::StringPattern::fromDivider("*;fehler"_el);
const auto [payload, suffix] = suffixPattern.split("probe-B8 leitwert=0.41;fehler"_el);
el::io::printLine("payload .................: "_el, payload);
el::io::printLine("suffix ..................: "_el, suffix);
}
Output
record id ...............: probe-A7
value ...................: temperatur=21.4C
payload .................: probe-B8 leitwert=0.41
suffix ..................: ;fehler
With a suffix-only pattern such as *;ok, the first result contains everything before the suffix and the second
contains the matched suffix.
With a pattern such as probe-??*;ok, the suffix is validated and omitted from the second result, leaving only the
useful middle part.
If the pattern does not match, the first result is empty and the second result contains the original text.
Match Lengths and Native String Indexes
length() and index() expose the native boundary position found by the matcher.
For UTF-8 strings, the result is a
ByteCount or
ByteIndex.
For UTF-16 strings, the values are UTF-16 code-unit positions.
For UTF-32 strings, they are code-point positions.
text/StringPattern/LengthAndIndex.cpp
/// `length()` and `index()` return native string positions for the matching part.
///
/// For UTF-8 text this means byte positions, for UTF-16 it means data-unit positions, and for UTF-32 it means
/// code-point positions. The values are meant for slicing and diagnostics near string boundaries, not for scanning an
/// entire grammar.
void lengthAndIndex() {
const auto microSensor = el::StringPattern::fromDivider(u8"µ?*"_el);
const auto u8Measurement = el::String{u8"µA=0.42"_el};
el::io::printLine("UTF-8 split index .......: "_el, microSensor.index(u8Measurement));
el::io::printLine("UTF-8 match bytes .......: "_el, microSensor.length(u8Measurement));
const auto suffix = el::StringPattern::fromDivider("*;ok"_el);
const auto accepted = el::String{"temperatur=21.4;ok"_el};
el::io::printLine("suffix index ............: "_el, suffix.index(accepted));
el::io::printLine("suffix length ...........: "_el, suffix.length(accepted));
}
Output
UTF-8 split index .......: 3
UTF-8 match bytes .......: 3
suffix index ............: 15
suffix length ...........: 3
These methods are useful when the next operation performs native string slicing, reports diagnostics near the validated boundary, or stores native string positions.
They are not intended to replace iteration.
If your algorithm needs to process every character or token, a search loop or
StringCharReader is usually the
better tool.
Complete demo sources
text/StringPattern/PatternConstruction.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/StringFormat.hpp>
#include <erbsland/StringList.hpp>
#include <erbsland/StringPattern.hpp>
namespace demo {
/// `StringPattern` parses a compact shell-like pattern once and reuses it for many text checks.
///
/// The parsed syntax is useful for small library-level recognizers: a literal prefix, one decoded character with `?`,
/// bracket character sets, and one `*` divider for front/back matching. Backslash escapes keep special characters
/// literal when they are part of the text format itself.
void patternConstruction() {
// A pattern can describe the small fixed shell around a laboratory record.
const auto probeRecord = el::StringPattern::fromDivider("probe-??[0-9]*"_el);
const auto probeLines = el::StringList{{
"probe-AB7 temperatur=21.4C"_el,
"probe-A7 temperatur=21.4C"_el,
}};
const auto lineFormat = el::StringFormat{"{:30}: {}"};
for (const auto &line : probeLines) {
el::io::printLine(lineFormat.build(line, probeRecord.matches(line)));
}
// Escapes make the pattern syntax available for literal diagnostic markers.
const auto marker = el::StringPattern::fromDivider("messung\\[\\?\\]\\*"_el);
el::io::printLine("escaped marker : "_el, marker.matches("messung[?]* archiviert"_el));
}
}
text/StringPattern/StaticConstruction.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/StringFormat.hpp>
#include <erbsland/StringList.hpp>
#include <erbsland/StringPattern.hpp>
namespace demo {
/// Typed construction builds a `StringPattern` from explicit pattern elements.
///
/// This form skips parsed pattern syntax. Literal text is supplied as UTF-32 through `Text`, and character choices are
/// expressed with `OneChar`, `Range`, and `Set`. `Divider` is the single front/back divider that corresponds to `*` in
/// parsed patterns.
void staticConstruction() {
using namespace el::pattern;
static const auto acceptedRecord = el::StringPattern::fromDivider(
Text{U"probe-"}, Range{U'A', U'Z'}, Set{{U'0', U'9'}}, OneChar{}, Divider{}, Text{U";ok"});
const auto probeLines = el::StringList{{
"probe-A7b wert=0.42;ok"_el,
"probe-a7b wert=0.42;ok"_el,
"probe-A7b wert=0.42;prüfen"_el,
}};
const auto lineFormat = el::StringFormat{"{:30}: {}"};
for (const auto &line : probeLines) {
el::io::printLine(lineFormat.build(line, acceptedRecord.matches(line)));
}
}
}
text/StringPattern/Matches.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>
#include <erbsland/StringPattern.hpp>
namespace demo {
/// `matches()` answers whether a text has the expected decoded-character shape.
///
/// A pattern without `*` matches the front of the string. A trailing `*` keeps that prefix behavior explicit.
/// A leading `*` checks the end. A pattern with text on both sides of `*` requires both the prefix and suffix without
/// overlap.
void matches() {
const auto nameOnly = el::StringPattern::fromDivider("messung:"_el);
const auto withUnit = el::StringPattern::fromDivider("messung:*;C"_el);
const auto finished = el::StringPattern::fromDivider("*;fertig"_el);
const auto temperature = el::String{"messung:temperatur=21.4;C"_el};
const auto completed = el::String{"messung:leitwert=0.42;fertig"_el};
el::io::printLine("front only ..............: "_el, nameOnly.matches(temperature));
el::io::printLine("front and back ..........: "_el, withUnit.matches(temperature));
el::io::printLine("suffix only .............: "_el, finished.matches(completed));
el::io::printLine("wrong suffix ............: "_el, finished.matches(temperature));
el::io::printLine("missing suffix ..........: "_el, withUnit.matches("messung:temperatur=21.4"_el));
}
}
text/StringPattern/TrimAndTrimmed.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>
#include <erbsland/StringEditor.hpp>
#include <erbsland/StringPattern.hpp>
namespace demo {
/// `trimmed()` and `trim()` remove the matched front or back shell of a string pattern.
///
/// For a pattern with text on both sides of the divider, the divider represents the part that stays. This is useful
/// when a protocol-like string wraps the meaningful value in a predictable prefix and suffix.
void trimAndTrimmed() {
const auto envelope = el::StringPattern::fromDivider("messung:*;ok"_el);
const auto original = el::String{"messung:temperatur=21.4C;ok"_el};
el::io::printLine("original ................: "_el, original);
el::io::printLine("trimmed .................: "_el, envelope.trimmed(original));
auto narrowed = el::StringEditor{"messung:ph=7.1;ok"_el};
if (envelope.trim(narrowed)) {
el::io::printLine("narrowed ................: "_el, narrowed);
}
auto unchanged = el::StringEditor{"messung:ph=7.1;prüfen"_el};
el::io::printLine("changed .................: "_el, envelope.trim(unchanged));
el::io::printLine("kept ....................: "_el, unchanged);
}
}
text/StringPattern/Split.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>
#include <erbsland/StringPattern.hpp>
namespace demo {
/// `split()` returns the matched text and the remaining text at the pattern boundary.
///
/// The method is especially handy when `?` or bracket sets make the matched prefix variable in bytes. For a pattern
/// with both a prefix and suffix, the suffix is validated and then dropped from the second result.
void split() {
const auto recordPattern = el::StringPattern::fromDivider("probe-??*;ok"_el);
const auto record = el::String{"probe-A7 temperatur=21.4C;ok"_el};
const auto [recordId, valueText] = recordPattern.split(record);
el::io::printLine("record id ...............: "_el, recordId);
el::io::printLine("value ...................: "_el, valueText);
const auto suffixPattern = el::StringPattern::fromDivider("*;fehler"_el);
const auto [payload, suffix] = suffixPattern.split("probe-B8 leitwert=0.41;fehler"_el);
el::io::printLine("payload .................: "_el, payload);
el::io::printLine("suffix ..................: "_el, suffix);
}
}
text/StringPattern/LengthAndIndex.cpp
// Copyright (c) 2026 Tobias Erbsland - https://erbsland.dev
// SPDX-License-Identifier: Apache-2.0
#include <DemoCommon.hpp>
#include <erbsland/StandardStreams.hpp>
#include <erbsland/String.hpp>
#include <erbsland/StringPattern.hpp>
namespace demo {
/// `length()` and `index()` return native string positions for the matching part.
///
/// For UTF-8 text this means byte positions, for UTF-16 it means data-unit positions, and for UTF-32 it means
/// code-point positions. The values are meant for slicing and diagnostics near string boundaries, not for scanning an
/// entire grammar.
void lengthAndIndex() {
const auto microSensor = el::StringPattern::fromDivider(u8"µ?*"_el);
const auto u8Measurement = el::String{u8"µA=0.42"_el};
el::io::printLine("UTF-8 split index .......: "_el, microSensor.index(u8Measurement));
el::io::printLine("UTF-8 match bytes .......: "_el, microSensor.length(u8Measurement));
const auto suffix = el::StringPattern::fromDivider("*;ok"_el);
const auto accepted = el::String{"temperatur=21.4;ok"_el};
el::io::printLine("suffix index ............: "_el, suffix.index(accepted));
el::io::printLine("suffix length ...........: "_el, suffix.length(accepted));
}
}