Lcl · Core · Packages · Libraries
RegexPOSIX extended regular expressions for Lcl.
regex.h (regcomp,
regexec, regfree)regex.h shim (for example a
third-party POSIX regex library).cmake -S . -B build -DLCL_BUILD_REGEX=ON
cmake --build buildAlways brace-quote patterns (
{^[0-9]+$}, never"^[0-9]+$"). Inside double quotes Lcl runs[...]through command substitution, so a quoted character class such as[0-9]is executed as the command0-9and the call fails withunknown command: 0-9. Braces are Lcl’s literal string syntax; every example below uses them.
All patterns use POSIX extended syntax (REG_EXTENDED);
matching is case-sensitive and byte-oriented, and every offset the
package reports is a 0-based byte offset with an exclusive end. Every
command that takes a pattern argument accepts
either a pattern string or a compiled handle from Regex::compile;
compile when the same pattern will run repeatedly. An invalid pattern
raises invalid regex pattern wherever it is used.
;; One-shot match
if [Regex::match {^[0-9]+$} $input] {
puts "input is digits"
}
;; Compile once, match many times
let re [Regex::compile {^https?://}]
foreach url $urls {
if [Regex::match $re $url] {
puts "web URL: $url"
}
}
;; No explicit free: the compiled regex is finalized when $re dies.
;; Captures: element 0 is the whole match, 1..n the groups
let caps [Regex::captures {(issue|bug)-([0-9]+)} "see issue-42"]
;; caps = ("issue-42" "issue" "42"), or () when no match
;; Rules as data: a pattern string is already data
let rules (
#{name "Issue reference" pattern {(issue|bug)-([0-9]+)} action open_issue}
)
;; Replace with group references
Regex::replace {(bug)-([0-9]+)} {\2 (\1)} "bug-42" ;; -> "42 (bug)"Regex::search
returns offset pairs and resumes from an optional from
offset, so a loop can walk a string without slicing it. Progress rule:
continue from end, or from start + 1 when the
match was zero-width.
let re [Regex::compile {[0-9]+}]
var pos 0
while {[<= $pos [len $text]]} {
let m [Regex::search $re $text $pos]
if [== [len $m] 0] { break }
let span [get $m 0]
puts "match at $span"
let so [get $span 0]
let eo [get $span 1]
if [> $eo $so] { set! pos $eo } else { set! pos [+ $so 1] }
}Regex::compile patternCompile pattern into a reusable opaque handle.
The handle prints as <opaque:regex_t> and can be
passed wherever a pattern is accepted. It is reference-counted: the
underlying regex_t is freed by the finalizer when the last
reference drops, so there is no explicit free.
Examples:
>> let re [Regex::compile {^(issue|bug)-[0-9]+$}]
>> type $re
"opaque"
>> Regex::match $re "issue-42"
1
>> Regex::match $re "task-9"
0
>> Regex::compile {[unclosed}
!! invalid regex patternRegex::regcomp patternAlias of Regex::compile
(POSIX-flavoured name).
Examples:
>> Regex::regexec [Regex::regcomp {ab+c}] "abbbc"
1Regex::regexec regex stringMatch a compiled handle against string; returns 1 or
0.
Unlike the other commands this one accepts only a handle from Regex::compile, never
a pattern string. Prefer Regex::match, which
accepts both.
Examples:
>> Regex::regexec [Regex::compile {ab+c}] "xabbbcx"
1
>> Regex::regexec [Regex::compile {ab+c}] "ac"
0
>> Regex::regexec {ab+c} "abc"
!! not a compiled regexRegex::match pattern stringReturn 1 if pattern matches anywhere in
string, else 0.
Anchor with ^ and $ to match the whole
string.
Examples:
>> Regex::match {^h.llo$} "hello"
1
>> Regex::match {^h.llo$} "hallo!"
0
>> Regex::match {[0-9]+} "abc123"
1
>> Regex::match {^[[:alpha:]]+$} "hello"
1
>> Regex::match {abc} "ABC"
0
>> Regex::match {(} "x"
!! invalid regex patternRegex::find pattern stringReturn (start end) byte offsets of the first match, or
().
end is exclusive. A zero-width match reports
start equal to end.
Examples:
>> Regex::find {[0-9]+} "ab123cd"
(2 5)
>> Regex::find {[0-9]+} "abcdef"
()
>> Regex::find [Regex::compile {b+}] "abbbc"
(1 4)
>> Regex::find {x*} "abc"
(0 0)Regex::captures pattern stringReturn the texts of the first match: the whole match, then each
capture group; () when nothing matches.
Groups are numbered by their opening parenthesis, left to right. An
optional group that did not participate in the match contributes an
empty string. Use Regex::search to get
offsets instead of copies.
Examples:
>> Regex::captures {(issue|bug)-([0-9]+)} "see issue-42 here"
("issue-42" "issue" "42")
>> Regex::captures {^([0-9]{4})-([0-9]{2})-([0-9]{2})$} "2026-08-29"
("2026-08-29" "2026" "08" "29")
>> Regex::captures {((a)(b))} "ab"
("ab" "ab" "a" "b")
>> Regex::captures {a(x)?b} "ab"
("ab" "")
>> Regex::captures {([0-9]+)} "abc"
()
>> let re [Regex::compile {([a-z]+)://(.+)}]
>> Regex::captures $re "https://example.com"
("https://example.com" "https" "example.com")Regex::search pattern string (from 0)Return offset pairs for the first match at or after byte
from: the whole match, then each capture group, as
(start end) pairs; () when nothing
matches.
Offsets are absolute (relative to the start of string,
not to from) and ends are exclusive; no text is copied. A
group that did not participate is (-1 -1). When
from is greater than 0 the position is not treated as a
line start, so ^ cannot match there; from
equal to the string length still lets $ match.
from past the end returns (); a negative or
non-integer from is an error. See the iteration recipe in
the overview for walking every match.
Examples:
>> Regex::search {(issue|bug)-([0-9]+)} "see issue-42 here"
((4 12) (4 9) (10 12))
>> Regex::search {[0-9]+} "a1b22c333" 2
((3 5))
>> Regex::search {[0-9]+} "a1b22c333" 6
((6 9))
>> Regex::search {[0-9]+} "abc"
()
>> Regex::search {a(x)?b} "ab"
((0 2) (-1 -1))
>> Regex::search {^[0-9]+} "1a2b" 2
()
>> Regex::search {$} "abc" 3
((3 3))
>> Regex::search {x*} "ab" 1
((1 1))
>> Regex::search {a} "abc" 10
()
>> Regex::search {a} "abc" -1
!! from must be >= 0Regex::find_all pattern stringReturn every non-overlapping whole-match text, left to right.
After the first match, later positions are not line starts, so
^ matches at most once. A zero-width match contributes an
empty string and scanning advances one byte.
Examples:
>> Regex::find_all {[0-9]+} "a1b22c333"
("1" "22" "333")
>> Regex::find_all {[a-z]+} "the quick brown"
("the" "quick" "brown")
>> Regex::find_all {[0-9]+} "abc"
()
>> Regex::find_all {^[0-9]+} "1a2b"
("1")
>> Regex::find_all {x*} "ab"
("" "" "")Regex::replace pattern replacement stringReturn string with every match replaced by
replacement.
In the replacement, \0 expands to the whole match and
\1..\9 to the capture groups (a group that did
not participate, or does not exist, expands to nothing); \\
is a literal backslash, and any other backslash sequence is kept as is.
Brace-quote the replacement so Lcl leaves the backslashes alone. A
zero-width match inserts the replacement between bytes.
Examples:
>> Regex::replace {[0-9]+} "N" "a1b22c"
"aNbNc"
>> Regex::replace {(bug)-([0-9]+)} {\2:\1} "bug-42"
"42:bug"
>> Regex::replace {[0-9]+} {<\0>} "a12b"
"a<12>b"
>> Regex::replace {[ ]+} " " "a b c"
"a b c"
>> Regex::replace {[0-9]+} "N" "abc"
"abc"
>> Regex::replace {a(x)?b} {<\1>} "ab"
"<>"
>> Regex::replace {a} {\\} "xax"
"x\\x"
>> Regex::replace {x*} "-" "ab"
"-a-b-"Regex::split pattern stringSplit string at every match; returns the substrings
between matches.
A match at the start or end contributes an empty leading or trailing field, and adjacent matches produce empty fields between them. Zero-width matches are not split points. With no match the result is the whole string as one element.
Examples:
>> Regex::split {,[ ]*} "a, b,c"
("a" "b" "c")
>> Regex::split {[[:space:]]+} "one two three"
("one" "two" "three")
>> Regex::split {,} "abc"
("abc")
>> Regex::split {,} ",a,"
("" "a" "")
>> Regex::split {,} "a,,b"
("a" "" "b")
>> Regex::split {,} ""
("")