Packages

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:

PatternMatches
\\d \\w \\sa 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|ba 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:

main.osl
import "std:regex"

log regex.match("\\d+", "order 66")
log regex.match("^\\d+$", "order 66")
log regex.match("^\\d+$", "66")
Click the code to edit it

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:

main.osl
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")
Click the code to edit it

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:

main.osl
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").len
Click the code to edit it

Index 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:

main.osl
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")
Click the code to edit it

When a fixed replacement isn't enough, replaceFunc calls a function for each match and uses what it returns:

main.osl
import "std:regex"

auto doubled = regex.replaceFunc("\\d+", "3 apples and 12 pears", n -> "" ++ (n.as<int> * 2))
log doubled
Click the code to edit it

Splitting#

split cuts text around every match, which handles separators that vary:

main.osl
import "std:regex"

log regex.split("[,;]\\s*", "a, b;c,   d")
log regex.split("\\s+", "split   on any   whitespace")
Click the code to edit it

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:

main.osl
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()
Click the code to edit it

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):

main.osl
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")
Click the code to edit it

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:

main.osl
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")
Click the code to edit it

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.

main.osl
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")
Click the code to edit it

The rest of the family: isValidPhone, isBase64.

Extracting things from text#

The extract functions scan free text and return what they find, in order:

main.osl
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!")
Click the code to edit it

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:

main.osl
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?")
Click the code to edit it

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.

main.osl
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:" slow
Click the code to edit it
Quick check

What does this print?

osl
log regex.match("^\\d+$", "order 66")
Your turn

Reformat phone numbers

Rewrite every number written as 5551234567 into (555) 123-4567, leaving the rest of the sentence alone.

main.osl
Ctrl Enter runs
Expected output
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) string

camelize turns snake_case text into camelCase, as in "hello_world" to "helloWorld".

#regex.charCount(string text, boolean includeSpaces) int

charCount 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) int

count returns how many non-overlapping matches of pattern text has. Throws ValueError for an invalid pattern.

#regex.escape(string text) string

escape returns text with every regular expression metacharacter escaped, so it matches itself literally inside a pattern.

#regex.extractEmail(any text) string

Deprecated. 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) string

Deprecated. 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) string

Deprecated. 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) boolean

isAlpha reports whether text is one or more ASCII letters.

#regex.isAlphanumeric(string text) boolean

isAlphanumeric reports whether text is one or more ASCII letters and digits.

#regex.isBase64(string text) boolean

isBase64 reports whether text is standard padded base64, ignoring line breaks. "" counts as valid.

#regex.isHexadecimal(string text) boolean

isHexadecimal reports whether text is one or more hex digits, with an optional 0x prefix.

#regex.isNumeric(string text) boolean

isNumeric reports whether text is one or more digits 0 to 9, with no sign or decimal point.

#regex.isUUID(string text) boolean

isUUID reports whether text is a UUID such as 3b241101-e2bb-4255-8caf-4136c566a962, in either case.

#regex.isValid(string pattern) boolean

isValid reports whether pattern is a valid regular expression.

#regex.isValidEmail(string email) boolean

isValidEmail reports whether email looks like [email protected]. It is a shape check, not proof that the address exists.

#regex.isValidIPv4(string ip) boolean

isValidIPv4 reports whether ip is an IPv4 address such as 192.168.0.1.

#regex.isValidIPv6(string ip) boolean

isValidIPv6 reports whether ip is an IPv6 address such as ::1.

#regex.isValidPhone(string phone) boolean

isValidPhone reports whether phone is 10 to 14 digits with an optional leading +, ignoring spaces, dashes and parentheses.

#regex.isValidURL(string url) boolean

isValidURL reports whether url is an http or https URL with a domain name, such as https://example.com/path.

#regex.kebabCase(string text) string

kebabCase turns camelCase or snake_case text into kebab-case, as in "helloWorld" to "hello-world".

#regex.maskEmail(string email) string

maskEmail hides an email address's name after its first character, as in "[email protected]" to "j***@example.com".

#regex.maskPhoneNumber(string phone) string

maskPhoneNumber replaces every character but the last four with *.

#regex.match(string pattern, string text) boolean

match 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) string

replace 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) string

replaceFunc 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) int

sentenceCount returns how many sentences text has: the pieces between ., ! and ? that contain more than whitespace.

#regex.slugify(string text) string

slugify 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) string

snakeCase 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) string

stripTags 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) string

stripWhitespace trims text and collapses each run of whitespace inside it to a single space.

#regex.test(any pattern) boolean

Deprecated. Deprecated: use regex.isValid(pattern) instead.

test reports whether pattern is a valid regular expression.

#regex.truncate(string text, int length, string suffix) string

truncate 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) int

wordCount returns how many words regex.extractWords finds in text.

*regex.Pattern

#pattern.count(string text) int

count 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) boolean

match reports whether the pattern matches anywhere in text.

#pattern.replace(string text, string replacement) string

replace 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) string

replaceFunc returns text with every match replaced by what fn returns when called with the matched text.

#pattern.source() string

source 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.