6 Regular Expressions
Concatenating two strings
Extracting substrings
Searching for substrings and replacing them
Names like Steve, Stephen, Steven, Stefan, or Esteve
Words containing an even number of vowels
For these kinds of tasks, we need regular expressions.
6.1 What Are Regular Expressions?
Searching and matching text
Formally describing strings
Defining the symbols (lexemes or tokens) that make up a language
Regular expressions are widely supported in modern programming languages and tools. They were popularized—and optimized for performance—as a language feature in Perl, and they are grounded in elegant theoretical foundations, which we will explore in a future lecture.
RE: A pure OCaml regular expression library that supports multiple formats (glob, POSIX, Str, etc.). In this lecture, we will use the POSIX format of the RE library.
Str: The standard OCaml Str module. This module is generally not recommended because it is relatively slow and does not support Unicode.
OCaml REPL
#require "re";; (* only needed in utop *) let str2re t = Re.Posix.compile (Re.Posix.re t);; let r = str2re "[a-z][0-9]+";; (* The pattern r matches substrings that start with a letter followed by one or more digits *) Re.matches r "a12#b22abcd";; [Output] val str2re : string -> Re__.Core.re = <fun> val r : Re__.Core.re = <abstr> - : string list = ["a12"; "b22"]
6.2 Basic Concepts
Regular expressions (regex) provide a formal way of describing patterns in strings. They form the basis for pattern matching, which we will implement in our next project.
6.2.1 Components of Regular Expressions
- Basic Building Blocks
Empty set (∅): Matches nothing
Empty string (ε): Matches an empty string
Symbols (character literals) from the alphabet Σ: Each symbol matches itself
- Operations on Building Blocks
Union (|): Provides a choice between patterns
Concatenation (· or implicit, i.e., writing patterns side by side): Joins patterns together
Kleene Star (*): Repeats a pattern zero or more times
Building Complex Patterns
By applying these operations recursively, we can construct arbitrarily complex patterns from the basic building blocks. This compositional structure makes regular expressions both powerful and mathematically elegant.
Examples:
"OCaml": Strings are matched exactly. It matches only "OCaml".
"a|b": A vertical bar separates alternatives (Boolean OR). It matches either "a" or "b".
"ab*": A quantifier (?, *, +, {n}) after an element specifies how many times the element can repeat. ab* matches an "a" followed by any number of "b" characters, for example "a", "ab", "abbbb".
6.2.2 Repetition in Regular Expressions
Pattern |
| Meaning |
| Example |
| Matches |
e* |
| Zero or more occurrences of e |
| a* |
| "",a,aa,aaa, ... |
e+ |
| One or more occurrences of e (same as ee*) |
| a+ |
| a,aa,aaa, ... |
e? |
| Zero or one occurrence of e |
| a? |
| "",a |
e{x} |
| Exactly x occurrences of e |
| a{3} |
| aaa |
e{x,} |
| At least x occurrences of e |
| a{2,} |
| aa,aaa, ... |
e{x,y} |
| At least x and at most y occurrences of e |
| a{2,4} |
| aa, aaa, aaaa |
bc* matches "b", "bc", "bcc", ...
a+b* matches "a", "ab", "aa", "aab", "aabb", "aabbb", "aaa", ...
Precedence: repetition binds more tightly than concatenation.
(OCaml)* matches "", "OCaml", "OCamlOCaml", ...
OCaml* matches "OCam", "OCaml", "OCamlllll", ...
Best practice: Use parentheses to avoid ambiguity. Note that parentheses also have another purpose: they can be used to capture matches (as we will see with capturing groups).
6.2.3 Character Classes
[abcd]: Matches any of {"a", "b", "c", "d"} (can also be written as (a|b|c|d))
[a-zA-Z0-9]: Matches any uppercase letter, lowercase letter or digit
[^0-9]: Matches any character except 0-9 (the ^ means not and must come first)
[[:space:]]: Matches any whitespace character, such as a space, tab, or newline
[a-zA-Z_\$][a-zA-Z_\$0-9]*: Matches Java identifiers ($ escaped)
Special Characters
^: Beginning of line
$: End of line
\$: Literal dollar sign
Note: Languages like Ruby and Python provide additional special characters.
- ^
Inside a character class: means "not" (e.g., [^0-9])
Outside a character class: means beginning of line
- ( )
Inside character classes: literal characters (e.g., [(ab)] matches a single (, a, b, or ), not the string ab)
Outside character classes: used for grouping
- -
Inside character classes: range (e.g., [a-z] means a to z)
Outside character classes: a literal hyphen
6.2.4 Summary of Regular Expression Syntax
re – matches regexp re
(re1|re2) – match either re1 or re2
(re)* – match 0 or more occurrences of re
(re)+ – match 1 or more occurrences of re
(re)? – match 0 or 1 occurrences of re
(re){2} – match exactly two occurrences of re
[a-z] – same as (a|b|c|...|z)
[^0-9] – match any character that is not a digit
^, $ – match start or end of string
You can experiment with regular expressions at rubular.com.
6.2.5 Regular Expression Examples
Now, we will learn regular expressions through practical examples.
Any string containing two consecutive abs (i.e., abab)
(ab){2}Any string containing a or two consecutive bs
a|bbContains sss or ccc
s{3}|c{3}Contains exactly 2 "b"s, not necessarily consecutive
^[^b]*b[^b]*b[^b]*$Starts with c, followed by one lowercase vowel, and ends with any number of lowercase letters
^c[aeiou][a-z]*$Starts with a and has exactly 0 or 1 letter after that
^a[A-Za-z]?$Only lowercase letters, in any amount, in alphabetic order
^a*b*c*d*e*f*g*h*i*j*k*l*m*n*o*p*q*r*s*t*u*v*w*x*y*z*$Contains one or more ab or ba
(ab|ba)+Precisely steve, steven, or stephen
^ste(ve|ven|phen)$Even length string
^(..)*$Even number of lowercase vowels
^[^aeiou]*([aeiou][^aeiou]*[aeiou][^aeiou]*)*$Starts with anything but "b", followed by one or more "a"’s and then no other characters
^[^b]a+$Quiz 1: How many different strings could the regex ^Hello, Anyone awake?$ match?
Quiz 1: How many different strings could the regex ^Hello, Anyone awake?$ match?
Answer: 2. The ? makes the preceding e optional, so the regex matches "Hello, Anyone awak" and "Hello, Anyone awake".
Quiz 2: Which regex is not equivalent to the others?
Quiz 2: Which regex is not equivalent to the others?
Answer: ^c?m?s?c?$. This regex can match the empty string and strings of up to 4 characters, while the others each match exactly one character from {c, m, s}.
Quiz 3: Which string does not match the regex [a-z]{4}[0-9]{3}?
Quiz 3: Which string does not match the regex [a-z]{4}[0-9]{3}?
Answer: "cmsc\d\d\d". The backslash and d are literal characters, not digits, so the [0-9]{3} portion is not satisfied.
6.3 Basic Functions in the Regular Expression Module Re
Module Re provides functions for creating and working with regular expressions, abstracting away the details of regular expression syntax. Below are the core functions:
matches: Returns all substrings in a string that match the regular expression as a list
compile: Converts a regular expression into a compiled object that can be used for matching
exec: Tests a string against a compiled regular expression and returns the captured groups, if any
split: Divides a string into segments using the regular expression as the separator
6.4 Extracting Substrings with Capturing Groups
In regular expressions, capturing groups are portions of the pattern enclosed in parentheses (). They let you extract specific parts of a matched string rather than just knowing that it matched. The Re library remembers which strings matched the parenthesized parts of a regular expression. These parts can be referred to as groups.
OCaml REPL
#require "re";; (* only needed in utop *) let str2re t = Re.Posix.compile (Re.Posix.re t);; let pattern = str2re "([0-9]{4})-([0-9]{2})-([0-9]{2})";; let text = "The date is 2026-02-23.";; let t = Re.exec pattern text;; Re.Group.get t 1;; (* year *) Re.Group.get t 2;; (* month *) Re.Group.get t 3;; (* day *) [Output] val str2re : string -> Re__.Core.re = <fun> val pattern : Re__.Core.re = <abstr> val text : string = "The date is 2026-02-23." val t : Re.Group.t = <abstr> - : string = "2026" - : string = "02" - : string = "23"
Example: Capturing Groups
let r = str2re "^Min:([0-9]+) Max:([0-9]+)$";;
let t = Re.exec r "Min:50 Max:99";;
let min = Re.Group.get t 1;; Ā (* 50 *)
let max = Re.Group.get t 2;; Ā (* 99 *)
Input:
Min:1 Max:27
Min:10 Max:30
Min: 11 Max: 30
Min: a Max: 24
Desired Output:
min=1 max=27
min=10 max=30By using capturing groups with parentheses, you can extract the numeric values and reformat the output accordingly. The last two input lines produce no output because they do not match the pattern (extra spaces, and a non-digit).
Quiz 4: What is the output of the following code?
let r = str2re "([A-Z]+)";;
let t = Re.exec r "HELP! Iām stuck";;
Re.Group.get t 1;;
Quiz 4: What is the output of the following code?
let r = str2re "([A-Z]+)";;
let t = Re.exec r "HELP! Iām stuck";;
Re.Group.get t 1;;Answer: "HELP". The capturing group ([A-Z]+) matches the leftmost run of uppercase letters (greedily, as long as possible) in "HELP! I’m stuck".
Quiz 5: What is the output of the following code?
let r = str2re "[0-9] ([A-Za-z]+).*([0-9])";;
let t = Re.exec r "Why was 6 afraid of 7?";;
Re.Group.get t 2;;
Quiz 5: What is the output of the following code?
let r = str2re "[0-9] ([A-Za-z]+).*([0-9])";;
let t = Re.exec r "Why was 6 afraid of 7?";;
Re.Group.get t 2;;Answer: "7". Group 2 captures the last ([0-9]), which matches "7" at the end of the substring "6 afraid of 7".
Quiz 6: What is the output of the following code?
let r = str2re "[A-Za-z]{2}";;
Re.matches r "Hello World";;
Quiz 6: What is the output of the following code?
let r = str2re "[A-Za-z]{2}";;
Re.matches r "Hello World";;Answer: ["He"; "ll"; "Wo"; "rl"]. The regex matches exactly 2 letters at a time non-overlappingly; single remaining letters like "o" and "d" are skipped.
Quiz 7: What is the output of the following code?
let r = str2re "[A-Za-z]+";;
Re.matches r "To be, or not to be!";;
Quiz 7: What is the output of the following code?
let r = str2re "[A-Za-z]+";;
Re.matches r "To be, or not to be!";;Answer: ["To"; "be"; "or"; "not"; "to"; "be"]. The regex matches runs of letters, stopping at non-letter characters like commas and !.
6.5 In-Class Example
dune build
dune exec bin/main.exe -- '^[^b]*b[^b]*b[^b]*$'The pattern ^[^b]*b[^b]*b[^b]*$ matches all words containing exactly two bs.
let () = if Array.length Sys.argv <> 2 then ( Printf.eprintf "Usage: %s <pattern>\n" Sys.argv.(0); exit 1); let pattern = Sys.argv.(1) in match Regex.match_words pattern with | words -> Regex.WordSet.iter (Printf.printf "%s\n") words | exception Re.Posix.Parse_error -> Printf.eprintf "Error: invalid pattern %S\n" pattern; exit 1 | exception Re.Posix.Not_supported -> Printf.eprintf "Error: pattern %S uses unsupported POSIX syntax\n" pattern; exit 1
(* Lists all words from "war and peace" that match the pattern More documentation about the Re library: https://ocaml.org/p/re/latest/doc/Re/index.html *) module WordSet = Set.Make (String) (* The corpus lives at the project root, but this may run from any subdirectory (e.g. under [dune test] or [dune exec]), so search upward from the current directory rather than assuming cwd is the root. *) let find_file name = let rec search dir = let candidate = Filename.concat dir name in if Sys.file_exists candidate then candidate else let parent = Filename.dirname dir in if parent = dir then failwith (Printf.sprintf "Could not locate %s" name) else search parent in search (Sys.getcwd ()) (* Read a file line by line*) let read_lines file = let contents = In_channel.with_open_bin (find_file file) In_channel.input_all in String.split_on_char '\n' contents let str2re t = Re.Posix.compile (Re.Posix.re t) let word_delimiter = str2re "[<>/,?!. '-()\":;*]" let split_into_words delimiter lines = List.concat_map (fun line -> Re.split delimiter line |> List.map String.lowercase_ascii) lines (* Lowercased words are read and split once per process and reused across calls, rather than re-reading and re-splitting the corpus every time. *) let corpus_words = lazy (split_into_words word_delimiter (read_lines "war_peace.txt")) (* Pure matching logic, independent of the corpus file, so it can be tested against a small in-memory word list instead of the full book. *) let words_matching pattern words = let re = str2re pattern in List.fold_left (fun acc word -> if Re.execp re word then WordSet.add word acc else acc) WordSet.empty words (* Return all unique, lowercased words from "war_peace.txt" matching [pattern] *) let match_words pattern = words_matching pattern (Lazy.force corpus_words)