regex
Find, capture, replace and split text with regular expressions, validate common formats, and reshape text with ready-made helpers.
import "std:regex"A regular expression (regex) is a small pattern language for text: "one or more digits", "a word followed by an @", "anything between two quotes". std:regex runs these patterns for you. Reach for it when plain contains and split aren't precise enough: pulling numbers out of a log line, checking that input has the right shape, or rewriting text by rule.
The syntax is RE2, the same as Go. It supports groups, classes, repetition and flags, and it runs in time proportional to the text, so a bad pattern can't hang your program. It has no backreferences or lookahead.
This page also covers the ready-made helpers (isValidEmail, slugify, extractURLs) that save you writing the pattern at all.
Writing patterns in strings#
A pattern is a normal string. Because \ starts an escape in an OSL string, write \\d to get the regex \d (a digit). Here are the building blocks used in this page:
| Pattern | Matches |
|---|---|
\\d \\w \\s | a digit, a word character, a space |
. | any character |
+ * ? | one or more, zero or more, optional |
{4} {2,3} | exactly 4, or 2 to 3 |
[abc] [a-z] [^0-9] | a character from a set, a range, or not in a set |
^ $ | start and end of the text |
( ) | a group you can capture |
a|b | a or b |
Does it match?#
regex.match(pattern, text) is true when the pattern matches anywhere in the text. Add ^ and $ to require that it matches the whole text:
import "std:regex"
log regex.match("\\d+", "order 66")
log regex.match("^\\d+$", "order 66")
log regex.match("^\\d+$", "66")Finding text#
findFirst returns the first match, or null when there is none, so ?? gives a default. findAll returns every match, and count says how many:
import "std:regex"
string text = "abc 123 def 45"
log regex.findFirst("\\d+", text)
log regex.findFirst("\\d+", "no digits") ?? "nothing"
log regex.findAll("\\d+", text)
log regex.count("a", "banana")Matches don't overlap, and they are found left to right.
Capturing parts#
Brackets make a group. findSubmatch returns the whole match, then what each group captured. It returns an empty array when nothing matches:
import "std:regex"
auto parts = regex.findSubmatch("(\\d{4})-(\\d{2})-(\\d{2})", "due on 2024-03-15 sharp")
log parts
log parts[1]
log parts[2] parts[3] parts[4]
log regex.findSubmatch("xyz", "nothing here").lenIndex 1 is the whole match and the groups follow, because arrays count from 1. Check parts.len before reading groups of text that might not match.
Replacing#
replace(pattern, text, replacement) replaces every match. In the replacement, $1, $2 and so on insert the captured groups, which lets you reorder text:
import "std:regex"
log regex.replace("\\s+", "too many \t spaces", " ")
log regex.replace("(\\w+)@(\\w+)", "ada@home", "$2:$1")
log regex.replace("(\\d{4})-(\\d{2})-(\\d{2})", "2024-03-15", "$3/$2/$1")When a fixed replacement isn't enough, replaceFunc calls a function for each match and uses what it returns:
import "std:regex"
auto doubled = regex.replaceFunc("\\d+", "3 apples and 12 pears", n -> "" ++ (n.as<int> * 2))
log doubledSplitting#
split cuts text around every match, which handles separators that vary:
import "std:regex"
log regex.split("[,;]\\s*", "a, b;c, d")
log regex.split("\\s+", "split on any whitespace")Compiled patterns#
The functions above read the pattern from a string on each call. When you use one pattern many times, or want to check it up front, compile it with regex.compile. It returns a result, failing with a ValueError if the pattern is invalid:
import "std:regex"
auto bad = regex.compile("(unclosed")
log bad.isErr()
log bad.unwrapErr().kind
auto date = regex.compile("(?P<year>\\d{4})-(?P<month>\\d{2})").unwrap()
log date.match("due 2024-03")
log date.findSubmatch("due 2024-03")
log date.groupNames()
log date.count("2024-01 and 2025-02")
log date.replace("2024-03", "$month/$year")
log date.source()A *regex.Pattern has the same operations as the functions (match, findFirst, findAll, findSubmatch, count, replace, replaceFunc, split), without the pattern argument. Named groups, written (?P<name>...), can be used in the replacement as $name.
regex.isValid(pattern) is a quick check when you only want to know if text is a legal pattern, for example one a user typed.
Flags#
Pass options to compile for case-insensitive or multi-line matching, or put the flags in the pattern as (?i):
import "std:regex"
auto ci = regex.compile("hello", {ignoreCase: true}).unwrap()
log ci.replace("Hello HELLO hello", "x")
auto lines = regex.compile("^\\w+", {multiline: true}).unwrap()
log lines.findAll("one two\nthree four")
log regex.replace("(?i)hello", "Hello HELLO", "x")The options are ignoreCase, multiline (^ and $ match at each line) and dotAll (. also matches a new line).
Escaping user text#
If you build a pattern from text you don't control, escape it first, so characters like . and * mean themselves:
import "std:regex"
string needle = "a.b*c"
log regex.escape(needle)
log regex.match(regex.escape(needle), "xx a.b*c xx")
log regex.match(needle, "xx aXbbbc xx")The third line shows the danger: unescaped, a.b*c is a pattern that matches aXbbbc.
Validating common formats#
Common checks come ready-made. They are practical sanity checks, not full standards: isValidEmail accepts anything shaped like [email protected], and the only real proof an address works is to send mail to it.
import "std:regex"
log regex.isValidEmail("[email protected]") regex.isValidEmail("not an email")
log regex.isValidURL("https://osl-lang.org") regex.isValidURL("osl-lang.org")
log regex.isValidIPv4("192.168.0.1") regex.isValidIPv6("::1")
log regex.isUUID("123e4567-e89b-12d3-a456-426614174000")
log regex.isNumeric("123") regex.isNumeric("12.5")
log regex.isAlpha("abc") regex.isAlphanumeric("abc123") regex.isHexadecimal("ff09")The rest of the family: isValidPhone, isBase64.
Extracting things from text#
The extract functions scan free text and return what they find, in order:
import "std:regex"
string post = [
"Thanks @ada! Docs at https://osl-lang.org/docs,",
"mail [email protected] #osl #docs, 3 or 12 tips."
].join(" ")
log regex.extractMentions(post)
log regex.extractHashtags(post)
log regex.extractURLs(post)
log regex.extractEmails(post)
log regex.extractNumbers(post)
log regex.extractWords("Hi, there!")Two things to notice. extractMentions skipped the @ inside [email protected]: a mention has to start the text or follow a character that can't be part of a name. And the URL kept its trailing comma, because the helper takes everything up to the next space. Trim the results yourself if your text has such cases, or write your own pattern with findAll.
Reshaping text#
Case and layout helpers cover everyday renaming and cleanup:
import "std:regex"
log regex.slugify("Hello World_Foo!")
log regex.camelize("hello_world") regex.snakeCase("helloWorld") regex.kebabCase("helloWorld")
log regex.truncate("A long sentence about regex", 11, "...")
log regex.stripTags("<p>Hello <b>there</b></p>")
log regex.stripWhitespace(" tidy up this ")
log regex.maskEmail("[email protected]") regex.maskPhoneNumber("5551234567")
log regex.wordCount("one two three") regex.sentenceCount("One. Two! Three?")slugify is what you want for URLs from titles. stripTags is a cleanup aid, not a security boundary: escape or sanitise HTML properly before showing untrusted text in a page.
Example: parsing log lines#
Putting it together: compile once, capture parts, and skip lines that don't fit.
import "std:regex"
auto line = regex.compile("^(\\w+) (\\d{3}) (\\d+)ms$").unwrap()
string[] logs = ["GET 200 12ms", "POST 500 340ms", "garbage", "GET 404 8ms"]
int slow = 0
for entry in logs (
auto parts = line.findSubmatch(entry)
if parts.len == 0 ( continue )
if parts[4].as<int> > 100 ( slow++ )
log parts[2] parts[3]
)
log "slow requests:" slowWhat does this print?
log regex.match("^\\d+$", "order 66")Reformat phone numbers
Rewrite every number written as 5551234567 into (555) 123-4567, leaving the rest of the sentence alone.
Call (555) 123-4567 or (555) 987-6543 today.
API reference#
54 members, generated from the std:regex source of OSL 9.3.0.
Functions
regex.camelize(string text) stringcamelize turns snake_case text into camelCase, as in "hello_world" to "helloWorld".
regex.charCount(string text, boolean includeSpaces) intcharCount returns how many characters text has, leaving out spaces unless includeSpaces is true.
regex.compile(string pattern, object? options) result<*regex.Pattern, error>compile compiles a pattern into a *regex.Pattern for repeated use. Use it for patterns that come from outside the program: an invalid pattern fails with ValueError instead of throwing. The syntax is RE2, as in Go: no backreferences or lookaround, and matching always takes linear time. Options: ignoreCase, multiline (^ and $ match at line breaks) and dotAll (. matches a line break), all false by default.
regex.count(string pattern, string text) intcount returns how many non-overlapping matches of pattern text has. Throws ValueError for an invalid pattern.
regex.escape(string text) stringescape returns text with every regular expression metacharacter escaped, so it matches itself literally inside a pattern.
regex.extractEmail(any text) stringDeprecated. Deprecated: use regex.extractEmails(text), which returns every address.
extractEmail returns the first email address in text, or "".
regex.extractEmails(string text) string[]extractEmails returns every email address in text, in order.
regex.extractHashtags(string text) string[]extractHashtags returns every #hashtag in text, including the #. A # in the middle of a word, as in a URL's page#section, starts no hashtag.
regex.extractMentions(string text) string[]extractMentions returns every @mention in text, including the @. An @ in the middle of a word, as in the email address [email protected], starts no mention.
regex.extractNumbers(string text) string[]extractNumbers returns every run of digits in text, as strings.
regex.extractURLs(string text) string[]extractURLs returns every http and https URL in text, in order.
regex.extractWords(string text) string[]extractWords returns every word in text: a run of letters in any language, which may contain an apostrophe, as in "don't" or "café".
regex.find(any pattern, any text) stringDeprecated. Deprecated: use regex.findFirst(pattern, text) instead, which returns null when nothing matches and throws ValueError for an invalid pattern.
find returns the first match of pattern in text, or "" when there is none or the pattern is invalid.
regex.findAll(string pattern, string text) string[]findAll returns every non-overlapping match of pattern in text, in order, or an empty array when there is none. Throws ValueError for an invalid pattern.
regex.findFirst(string pattern, string text) string?findFirst returns the first match of pattern in text, or null when there is none. Throws ValueError for an invalid pattern.
regex.findSubmatch(string pattern, string text) string[]findSubmatch returns the first match of pattern in text followed by the text of each capture group, "" for a group that didn't take part, or an empty array when nothing matches. Throws ValueError for an invalid pattern.
regex.highlight(any text, any pattern, any color) stringDeprecated. Deprecated: use regex.replace(pattern, text, "\x1b[31m$0\x1b[0m") with the color code you want.
highlight wraps each match of pattern in text with an ANSI color code, returning text unchanged for an unknown color or invalid pattern.
regex.isAlpha(string text) booleanisAlpha reports whether text is one or more ASCII letters.
regex.isAlphanumeric(string text) booleanisAlphanumeric reports whether text is one or more ASCII letters and digits.
regex.isBase64(string text) booleanisBase64 reports whether text is standard padded base64, ignoring line breaks. "" counts as valid.
regex.isHexadecimal(string text) booleanisHexadecimal reports whether text is one or more hex digits, with an optional 0x prefix.
regex.isNumeric(string text) booleanisNumeric reports whether text is one or more digits 0 to 9, with no sign or decimal point.
regex.isUUID(string text) booleanisUUID reports whether text is a UUID such as 3b241101-e2bb-4255-8caf-4136c566a962, in either case.
regex.isValid(string pattern) booleanisValid reports whether pattern is a valid regular expression.
regex.isValidEmail(string email) booleanisValidEmail reports whether email looks like [email protected]. It is a shape check, not proof that the address exists.
regex.isValidIPv4(string ip) booleanisValidIPv4 reports whether ip is an IPv4 address such as 192.168.0.1.
regex.isValidIPv6(string ip) booleanisValidIPv6 reports whether ip is an IPv6 address such as ::1.
regex.isValidPhone(string phone) booleanisValidPhone reports whether phone is 10 to 14 digits with an optional leading +, ignoring spaces, dashes and parentheses.
regex.isValidURL(string url) booleanisValidURL reports whether url is an http or https URL with a domain name, such as https://example.com/path.
regex.kebabCase(string text) stringkebabCase turns camelCase or snake_case text into kebab-case, as in "helloWorld" to "hello-world".
regex.maskEmail(string email) stringmaskEmail hides an email address's name after its first character, as in "[email protected]" to "j***@example.com".
regex.maskPhoneNumber(string phone) stringmaskPhoneNumber replaces every character but the last four with *.
regex.match(string pattern, string text) booleanmatch reports whether pattern matches anywhere in text. Throws ValueError for an invalid pattern; use regex.compile for patterns from outside the program.
regex.replace(string pattern, string text, string replacement) stringreplace returns text with every match of pattern replaced by replacement, in which $1 or ${name} stands for a capture group and $$ for a literal $. Throws ValueError for an invalid pattern.
regex.replaceFunc(string pattern, string text, any fn) stringreplaceFunc returns text with every match of pattern replaced by what fn returns when called with the matched text. Throws ValueError for an invalid pattern.
regex.sentenceCount(string text) intsentenceCount returns how many sentences text has: the pieces between ., ! and ? that contain more than whitespace.
regex.slugify(string text) stringslugify lowercases text, turns spaces and underscores into dashes, and drops everything but ASCII letters, digits and single dashes, as in "Hello World!" to "hello-world".
regex.snakeCase(string text) stringsnakeCase turns camelCase or kebab_case text into snake_case, as in "helloWorld" to "hello_world".
regex.split(string pattern, string text) string[]split splits text around every match of pattern; text without a match comes back whole as the only item. Throws ValueError for an invalid pattern.
regex.stripTags(string text) stringstripTags removes everything that looks like an HTML tag from text. It is not an HTML sanitizer: don't rely on it to make input safe.
regex.stripWhitespace(string text) stringstripWhitespace trims text and collapses each run of whitespace inside it to a single space.
regex.test(any pattern) booleanDeprecated. Deprecated: use regex.isValid(pattern) instead.
test reports whether pattern is a valid regular expression.
regex.truncate(string text, int length, string suffix) stringtruncate shortens text to its first length characters and appends suffix, or returns text unchanged when it is no longer than length.
regex.wordCount(string text) intwordCount returns how many words regex.extractWords finds in text.
*regex.Pattern
pattern.count(string text) intcount returns how many non-overlapping matches text has.
pattern.findAll(string text) string[]findAll returns every non-overlapping match in text, in order.
pattern.findFirst(string text) string?findFirst returns the first match in text, or null when there is none.
pattern.findSubmatch(string text) string[]findSubmatch returns the first match in text followed by the text of each capture group, or an empty array when nothing matches.
pattern.groupNames() string[]groupNames returns the names of the pattern's capture groups in order, "" for an unnamed group.
pattern.match(string text) booleanmatch reports whether the pattern matches anywhere in text.
pattern.replace(string text, string replacement) stringreplace returns text with every match replaced by replacement, in which $1 or ${name} stands for a capture group and $$ for a literal $.
pattern.replaceFunc(string text, any fn) stringreplaceFunc returns text with every match replaced by what fn returns when called with the matched text.
pattern.source() stringsource returns the pattern's source text, including any (?flags) the options added.
pattern.split(string text) string[]split splits text around every match; text without a match comes back whole as the only item.