Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Swift  Module 3 – Strings, text and collections

Regex literals and RegexBuilder

Match and capture text with Swift regex literals and typed captures, build patterns with RegexBuilder, and keep them fast and buildable on older macOS.

  • Intermediate
  • 20 minutes
  • Examples run with Swift 6.4 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Match and capture text with regex literals and their typed, named captures
  • Build readable patterns with RegexBuilder, and turn a capture into an Int with TryCapture
  • Use firstMatch, wholeMatch, matches, replacing and split with a Regex, and validate input with wholeMatch
  • Recognise and fix catastrophic backtracking, and set the macOS version that Regex needs

Before you start

What you’ll learn · Before you start

What you will learn

  • Match and capture text with regex literals and their typed, named captures
  • Build readable patterns with RegexBuilder, and turn a capture into an Int with TryCapture
  • Use firstMatch, wholeMatch, matches, replacing and split with a Regex, and validate input with wholeMatch
  • Recognise and fix catastrophic backtracking, and set the macOS version that Regex needs

Before you start

On this page

The alert “Debited 1,250.00 from account XX4321. Status: SUCCESS” holds three facts, and one regex literal pulls all three out as named values. Written carelessly, the same tool tried 2,097,151 times to match twenty digits and an x before it gave up, where a better pattern needed 21 tries.

Here you will match and capture text with regex literals, write the same patterns as Swift code with RegexBuilder and turn captures into numbers, search, validate, replace and split strings with a Regex, find and fix a pattern that backtracks without end, and build Regex code for the macOS versions that have it.

A regex literal is a pattern the compiler checks

A regular expression: A pattern that describes the text it matches, such as [A-Z]{3}\d{6} for three capital letters and six digits. Swift's Regex type matches one against a string and returns the parts it captured. describes the text it matches: \d is a digit, [A-Z]+ one or more capital letters, X+\d{4} one or more X followed by four digits. In Swift it is written as a literal between #/ and /#, and each capture: A part of a regex in brackets, (…) or (?<name>…), whose matched text is returned in the match's output. Swift types it as a Substring, or as the value a TryCapture transform returns. (?<name>…) becomes a named part of the result. This program reads an alert:

alert.swift: three named captures from one alert Swift · alert.swift
// alert.swift: one regex literal pulls three facts out of an alert.
let alert = "Debited 1,250.00 from account XX4321. Status: SUCCESS"
let pattern = #/(?<amount>\d[\d,]*\.\d\d) from account (?<account>X+\d{4})\. Status: (?<status>[A-Z]+)/#

if let match = alert.firstMatch(of: pattern) {
    print("amount:", match.output.amount)
    print("account:", match.output.account)
    print("status:", match.output.status)
    print("whole match:", match.output.0)
}
print(type(of: pattern))
let other = "Credited 99.00 to account XX9876"
print("another alert:", other.firstMatch(of: pattern) == nil ? "firstMatch returned nil" : "matched")

Output

amount: 1,250.00
account: XX4321
status: SUCCESS
whole match: 1,250.00 from account XX4321. Status: SUCCESS
Regex<(Substring, amount: Substring, account: Substring, status: Substring)>
another alert: firstMatch returned nil

Recorded with Apple Swift 6.4 (swiftlang-6.4.0.34.1) on macOS 26 arm64. To run it yourself: swiftc -O alert.swift -o alert && ./alert

firstMatch(of:) returns an optional: the first match in the text, or nil, as for the other alert. The match’s output is a tuple whose types the compiler worked out from the pattern itself: the whole match first, then amount, account and status, each a Substring of the alert. Step through how the pattern lines up with the text:

Interactive · step by step How the alert pattern lines up with the alert Step through the match of alert.swift. The left column is each piece of the pattern; the right column is the text it matched.
How the alert pattern lines up with the alert, step 6 of 6. Showing amount: \d[\d,]*\.\d\d, " from account ", account: X+\d{4}, "\. Status: ", status: [A-Z]+, 1,250.00, from account , XX4321, . Status: , SUCCESS.amount: \d[\d,]*\.\d\da named capture" from account "plain textaccount: X+\d{4}a named capture"\. Status: "plain textstatus: [A-Z]+a named capture1,250.00output.amount from account matchedXX4321output.account. Status: matchedSUCCESSoutput.status

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Find a start. firstMatch tries the pattern at each position of the alert from the start. At D, e, b and the rest of "Debited " the first piece needs a digit and fails, so the engine moves on.
  2. The amount. At the 1 the amount piece fits: a digit, then digits and commas, a point and two digits, 1,250.00. Its text becomes output.amount.
  3. Plain text. " from account " must follow exactly, and it does. Plain text in a pattern matches itself.
  4. The account. X+ takes both X characters and \d{4} the four digits, so output.account is XX4321.
  5. The status. \. matches the full stop itself, the backslash making it plain, then " Status: ", and [A-Z]+ takes SUCCESS as output.status.
  6. Typed output. The match ends there. Its output is a tuple of Substrings, (whole match, amount, account, status), with the names the pattern gave them.

Because the compiler reads the pattern, a broken one is a compile error, not a surprise at run time. The other form of the literal, between plain slashes, /\d+/, needs the Swift 6 language mode (SE-0354); in Swift 5 mode, the default of a plain swiftc, the compiler reads the slashes as operators, and the extended form #/…/# is the one that works. This session builds the two files below:

The two files the session builds

bad_pattern.swift

// bad_pattern.swift: a regex literal with a bracket that is never closed.
let reference = #/Ref (\d+/#
print("Ref 4417".contains(reference))

bare.swift

// bare.swift: a regex literal between plain slashes.
let code = "Ref 4417"
let digits = /\d+/
print(code.contains(digits))
A pattern with an unclosed bracket, and /…/ in two language modes Python · literal_session.py
from session import run, version

# bad_pattern.swift has an unclosed bracket; bare.swift uses /…/, which needs the Swift 6 language mode.
version()
run("""
!swiftc bad_pattern.swift -o bad_pattern
swiftc -swift-version 6 bare.swift -o bare
./bare
!swiftc bare.swift -o bare
""", max_lines=8)

Output

# Apple Swift version 6.4 (swiftlang-6.4.0.34.1)
$ swiftc bad_pattern.swift -o bad_pattern
bad_pattern.swift:2:27: error: cannot parse regular expression: expected ')'
1 | // bad_pattern.swift: a regex literal with a bracket that is never closed.
2 | let reference = #/Ref (\d+/#
  |                           `- error: cannot parse regular expression: expected ')'
3 | print("Ref 4417".contains(reference))
4 | 
[exit status 1]
$ swiftc -swift-version 6 bare.swift -o bare
$ ./bare
true
$ swiftc bare.swift -o bare
bare.swift:3:14: error: '/' is not a prefix unary operator
1 | // bare.swift: a regex literal between plain slashes.
2 | let code = "Ref 4417"
3 | let digits = /\d+/
  |              `- error: '/' is not a prefix unary operator
4 | print(code.contains(digits))
5 | 

[23 more lines]
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 literal_session.py

The first build stopped at the pattern with “expected ‘)’”; bare.swift compiled and printed true in Swift 6 mode, and failed in Swift 5 mode. A package whose manifest says swift-tools-version: 6.0 builds its targets in Swift 6 mode unless the package or a target asks for another, as the Swift 6 migration guide states, so both forms work there.

Checkpoint

Check: Regex literals and typed captures

  1. Question 1 of 2 What is the type of #/(?<code>[A-Z]{3})-(?<seat>\d+)/#?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Regex<(Substring, code: Substring, seat: Substring)>

    The compiler reads the literal and works out its output type, so match.output.code and match.output.seat are typed and checked.

  2. Question 2 of 2 A single file built with a plain swiftc file.swift uses the literal /\d+/. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It does not compile, because the Swift 5 mode reads the slashes as operators; write #/\d+/# or build with -swift-version 6

    #/…/# works in every language mode; /…/ needs Swift 6 mode, which a package with swift-tools-version 6.0 has.

RegexBuilder spells a pattern as Swift code

A pattern of many symbols is hard to read and harder to change. RegexBuilder (SE-0351) writes the same pattern as Swift values: Repeat(letter, count: 4) for [A-Z]{4}, Capture { … } for a bracket, ChoiceOf { … } for alternatives, OneOrMore, Optionally and CharacterClass for the rest. This program checks codes shaped like an IFSC, which names a bank branch in India’s NEFT payments: four letters for the bank, a zero, then six letters or digits for the branch, as the Reserve Bank of India describes it. The codes in the program are made up. Then it reads debits:

builder.swift: branch codes and debit amounts with RegexBuilder Swift · builder.swift
// builder.swift: patterns written as Swift code with RegexBuilder, with a typed capture.
import RegexBuilder

let letter = CharacterClass("A"..."Z")
let letterOrDigit = CharacterClass("A"..."Z", "0"..."9")
let branchCode = Regex {
    Capture { Repeat(letter, count: 4) }
    "0"
    Capture { Repeat(letterOrDigit, count: 6) }
}
for code in ["ABCB0001234", "XYZB0A12B3C", "ABCB1234567", "abcb0001234", "ABCB00012345"] {
    if let match = code.wholeMatch(of: branchCode) {
        print(code, "->", match.1, match.2)
    } else {
        print(code, "-> no match")
    }
}

let debit = Regex {
    ChoiceOf {
        "Debited "
        "Debit of "
    }
    TryCapture {
        OneOrMore(CharacterClass(.digit, .anyOf(",")))
        "."
        Repeat(.digit, count: 2)
    } transform: { Int($0.filter(\.isNumber)) }
}
for text in ["Debited 1,250.00 from XX4321", "Debit of 99.00 from XX4321 failed", "Debited one rupee"] {
    if let match = text.firstMatch(of: debit) {
        print("paise:", match.1, "a value of type", type(of: match.1))
    } else {
        print("no debit in:", text)
    }
}

Output

ABCB0001234 -> ABCB 001234
XYZB0A12B3C -> XYZB A12B3C
ABCB1234567 -> no match
abcb0001234 -> no match
ABCB00012345 -> no match
paise: 125000 a value of type Int
paise: 9900 a value of type Int
no debit in: Debited one rupee

Recorded with Apple Swift 6.4 (swiftlang-6.4.0.34.1) on macOS 26 arm64. To run it yourself: swiftc -O builder.swift -o builder && ./builder

Step through how each builder component matches a part of the literal syntax:

Interactive · step by step RegexBuilder components and the literal syntax they stand for Step through builder.swift's two patterns. The left column is the builder code; the right column is the same piece written as a literal.
RegexBuilder components and the literal syntax they stand for, step 6 of 6. Showing Repeat(letter, count: 4), "0", Repeat(…, count: 6), ChoiceOf { … }, TryCapture { … }, ([A-Z]{4}), 0, ([A-Z0-9]{6}), (?:Debited |Debit of ), ([\d,]+\.\d\d).Repeat(letter, count: 4)inside Capture"0"plain textRepeat(…, count: 6)letterOrDigit, in CaptureChoiceOf { … }"Debited " or "Debit of "TryCapture { … }transform: Int(…)([A-Z]{4})a capture0plain text([A-Z0-9]{6})a capture(?:Debited |Debit of )alternatives([\d,]+\.\d\d)then convert it yourself

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Four letters. letter is CharacterClass("A"..."Z"). Repeat(letter, count: 4) inside Capture is ([A-Z]{4}) in a literal: four capital letters, captured as match.1.
  2. A zero. A string in a builder matches itself, so "0" is the literal 0.
  3. Six letters or digits. letterOrDigit is CharacterClass("A"..."Z", "0"..."9"), the class [A-Z0-9]; six of them, captured, are match.2.
  4. Alternatives. ChoiceOf lists alternatives, one per line, like a group with | between them. The debit pattern accepts "Debited " or "Debit of ".
  5. Capture and convert. TryCapture matches digits and commas, a point and two digits, then runs its transform on the text: Int of the digits alone, 125000. A literal can only capture the text, which you would convert afterwards.
  6. Typed results. branchCode gives (Substring, Substring, Substring) and the debit pattern (Substring, Int): the types follow the components.

TryCapture captures and converts in one step: its transform closure turned “1,250.00” into the Int 125000, the amount in paise. When a transform returns nil, the engine backtracks and tries another way, and the match fails when no way is left, so a capture that is not a number never gets through. The match’s .1 is an Int, not a Substring. The lower-case code failed the letter class, the code without its zero failed the "0", and the code with one character too many failed because wholeMatch must cover the whole string, as the next section shows.

Mix the two forms

A builder can hold literals: Regex { #/\d{4}/#; "-"; Capture { OneOrMore(.word) } }. Keep the short, familiar pieces as literals and write the parts that need names, transforms or comments as builder code.

Checkpoint

Check: RegexBuilder and TryCapture

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    import RegexBuilder
    let seat = Regex {
        "Seat "
        TryCapture { OneOrMore(.digit) } transform: { Int($0) }
    }
    if let m = "Seat 34, coach B2".firstMatch(of: seat) {
        print(m.1 + 1)
    }
    Show the answer: Question 1 of 2

    Answer: 35

    A transform in TryCapture or Capture changes the capture's type, so the conversion happens during matching.

  2. Question 2 of 2 Which RegexBuilder component stands for [0-9]{6}?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Repeat(.digit, count: 6)

    Repeat(_:count:) is {n}, OneOrMore is +, ZeroOrMore is * and Optionally is ?.

String methods take a Regex, and wholeMatch validates

The string methods of SE-0357 take a Regex: contains, ranges, replacing, split and trimmingPrefix take one where they also take plain text, and firstMatch, wholeMatch, prefixMatch and matches return the matches themselves. This program uses them on booking references and a message about cards:

methods.swift: matching, replacing and splitting with a Regex Swift · methods.swift
// methods.swift: String methods that take a Regex.
let reference = #/[A-Z]{3}\d{6}/#
for text in ["PNR442177", "pnr442177", "PNR442177 and more", "Ref PNR442177"] {
    let whole = text.wholeMatch(of: reference) != nil
    let first = text.firstMatch(of: reference)?.output ?? "none"
    print(text, "| wholeMatch:", whole, "| firstMatch:", first)
}

let message = "Card XX4321 paid 450.75; card XX9876 refunded 99.00"
print(message.replacing(#/XX(\d{4})/#) { match in "account ending \(match.1)" })
print(message.matches(of: #/\d+\.\d\d/#).map(\.output))
print(message.contains(#/refunded/#), message.ranges(of: #/XX\d{4}/#).count)
print("Asha ,Ravi,  Meera".split(separator: #/\s*,\s*/#))
print("Ref: 4417".trimmingPrefix(#/Ref:\s*/#))

Output

PNR442177 | wholeMatch: true | firstMatch: PNR442177
pnr442177 | wholeMatch: false | firstMatch: none
PNR442177 and more | wholeMatch: false | firstMatch: PNR442177
Ref PNR442177 | wholeMatch: false | firstMatch: PNR442177
Card account ending 4321 paid 450.75; card account ending 9876 refunded 99.00
["450.75", "99.00"]
true 2
["Asha", "Ravi", "Meera"]
4417

Recorded with Apple Swift 6.4 (swiftlang-6.4.0.34.1) on macOS 26 arm64. To run it yourself: swiftc -O methods.swift -o methods && ./methods

firstMatch found PNR442177 inside “Ref PNR442177” and inside “PNR442177 and more”, so it is the wrong test for “is this a booking reference?”. wholeMatch succeeded only when the pattern covered the whole string. OWASP’s Input Validation Cheat Sheet asks for exactly that when a regex validates a field: require a match of the entire value. Step through the four references:

Interactive · step by step wholeMatch or firstMatch on four booking references Step through the four texts of methods.swift. The left column is each text; the right column is what wholeMatch and firstMatch return for it.
wholeMatch or firstMatch on four booking references, step 5 of 5. Showing PNR442177, pnr442177, PNR442177 and more, Ref PNR442177, whole: true, whole: false, whole: false, whole: false.PNR442177pnr442177PNR442177 and moreRef PNR442177whole: truefirst: PNR442177whole: falsefirst: nonewhole: falsefirst: PNR442177whole: falsefirst: PNR442177

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. An exact reference. PNR442177 is three capital letters and six digits and nothing else, so both methods succeed.
  2. Lower case. [A-Z] means capital letters only, so pnr442177 matches nowhere: both methods fail.
  3. Extra text after it. firstMatch finds PNR442177 at the start and ignores " and more". wholeMatch must reach the end of the string, and the extra text stops it.
  4. Extra text before it. firstMatch tries each position until one fits, at P, so it finds the reference after "Ref ". wholeMatch starts at R, where the pattern fails.
  5. Validate with wholeMatch. Only wholeMatch answers "is this text a booking reference?". A validator built on firstMatch would accept the last two texts.

replacing took a closure that builds each replacement from the match, so both card numbers became “account ending” and their last four digits. matches(of:) returned every amount, split cut at commas with any spaces around them, and trimmingPrefix removed a label from the front.

Key idea

Use firstMatch to find a pattern inside text, and wholeMatch to decide whether a whole value is valid.

Checkpoint

Check: String methods that take a Regex

  1. Question 1 of 2 A form field must accept a six-digit postal code and nothing else. Which test is right?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: code.wholeMatch(of: #/\d{6}/#) != nil

    Validation requires a match of the entire value, as OWASP's guidance says; wholeMatch is that test.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    print("XX4321 and XX9876".replacing(#/XX\d{4}/#, with: "XX****"))
    Show the answer: Question 2 of 2

    Answer: XX** and XX**

    replacing(_:with:) returns a copy with every match replaced; a closure can build each replacement.

Nested quantifiers can try every split of the input

Swift’s Regex is a backtracking: What a regex engine does when part of a pattern fails: it returns to an earlier choice, such as how many characters a + took, and tries another. Nested quantifiers can make the choices grow exponentially. engine: when part of a pattern fails, it goes back to an earlier choice, such as how many digits a + took, and tries another. In ^(\d+)+$, the inner \d+ and the outer + can share out a row of digits in every possible way, and when the text ends in something that is not a digit, every way fails and every way is tried. This program counts the single-digit tries of three patterns, with a component that adds one to a tally each time the engine asks it for a digit:

backtracking.swift: single-digit tries of three patterns on digits and an x Swift · backtracking.swift
// backtracking.swift: how many single-digit tries three patterns make on digits followed by an x.
import RegexBuilder

final class Tally {
    var tries = 0
}

/// Matches one ASCII digit, and counts every time the regex engine asks it to.
struct CountedDigit: CustomConsumingRegexComponent {
    typealias RegexOutput = Substring
    let tally: Tally

    func consuming(
        _ input: String, startingAt index: String.Index, in bounds: Range<String.Index>
    ) throws -> (upperBound: String.Index, output: Substring)? {
        tally.tries += 1
        guard index < bounds.upperBound, input[index].isASCII, input[index].isNumber else {
            return nil
        }
        let next = input.index(after: index)
        return (next, input[index..<next])
    }
}

for length in [3, 5, 10, 15, 20] {
    let text = String(repeating: "1", count: length) + "x"
    let nestedTally = Tally()
    let flatTally = Tally()
    let atomicTally = Tally()
    let nested = Regex { OneOrMore { OneOrMore { CountedDigit(tally: nestedTally) } } }  // like ^(\d+)+$
    let flat = Regex { OneOrMore { CountedDigit(tally: flatTally) } }  // like ^\d+$
    let atomic = Regex { OneOrMore { Local { OneOrMore { CountedDigit(tally: atomicTally) } } } }  // like ^(?>\d+)+$
    let matched = [text.wholeMatch(of: nested), text.wholeMatch(of: flat), text.wholeMatch(of: atomic)].contains { $0 != nil }
    print(
        "\(length) digits: nested \(nestedTally.tries) tries, flat \(flatTally.tries),",
        "atomic \(atomicTally.tries), any match: \(matched)")
}

Output

3 digits: nested 15 tries, flat 4, atomic 5, any match: false
5 digits: nested 63 tries, flat 6, atomic 7, any match: false
10 digits: nested 2047 tries, flat 11, atomic 12, any match: false
15 digits: nested 65535 tries, flat 16, atomic 17, any match: false
20 digits: nested 2097151 tries, flat 21, atomic 22, any match: false

Recorded with Apple Swift 6.4 (swiftlang-6.4.0.34.1) on macOS 26 arm64. To run it yourself: swiftc -O backtracking.swift -o backtracking && ./backtracking

Each extra digit doubled the nested pattern’s work: 63 tries for 5 digits, 2,047 for 10 and 2,097,151 for 20, which is 2 to the power of (n + 1), minus 1. The flat pattern ^\d+$ needed n + 1 tries. Step through the four ways the nested pattern shares out three digits:

Interactive · step by step Four ways for (\d+)+ to share out 111 before the x Step through the nested pattern on 111x, as the engine backtracks. The left column is each way of splitting the digits; the right column is how it ends.
Four ways for (\d+)+ to share out 111 before the x, step 6 of 6. Showing (111), (11)(1), (1)(11), (1)(1)(1), fails at x, fails at x, fails at x, fails at x. In focus: (111).(111)one group of three(11)(1)two(1)(11)one(1)(1)(1)three groups of onefails at xfails at xfails at xfails at x

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Greedy first. The inner \d+ takes all three digits, the outer + has one group, and the whole match needs the end of the text next. It finds the x, so this way fails.
  2. Give one back. The engine backtracks: the inner \d+ gives back the last digit, and the outer + starts a second group with it. The x is still next, so this fails too.
  3. Another split. Back again: the first group takes one digit and a second group takes two. Same x, same failure.
  4. Smallest groups. The last way is three groups of one digit. It fails at the x as well, and no way is left: no match.
  5. Doubling. 3 digits can be split 4 ways, and each extra digit doubles the ways, because it can join the group before it or start a new one. backtracking.swift counted 15 single-digit tries here, and 2,097,151 for 20 digits.
  6. The flat pattern. \d+ alone has one way to take the digits, so after failing at the x it gives back digits without trying new splits: 4 tries for 3 digits, 21 for 20.

Two fixes keep the meaning and remove the explosion. Remove the nesting, as the flat pattern does, when the brackets add nothing. Or make the inner part an atomic group: A part of a regex, (?>…) or RegexBuilder's Local, that keeps the first match it finds and drops its other choices, so the engine cannot retry it in other ways after a later failure.: (?>\d+) in a literal, Local in RegexBuilder, or a possessive quantifier such as \d++. An atomic group keeps the match it found and gives up its other choices, which made the third pattern linear: n + 2 tries.

Patterns that read untrusted text

A server that runs a nested pattern on text from users can be stalled by one long, carefully chosen string. Bound the length before matching, as OWASP advises, prefer patterns without nested quantifiers, and test them with long inputs that almost match.

Checkpoint

Check: Backtracking and how to stop it

  1. Question 1 of 2 In backtracking.swift the nested pattern made 2^(n + 1) − 1 single-digit tries for n digits and an x. How many for 8 digits?

    Type a number.

    A hint

    2^9 is 512.

    Show the answer: Question 1 of 2

    Answer: 511 tries

    2^(8 + 1) − 1 = 511, where the flat pattern makes 9. Every extra digit doubles the nested pattern's work.

  2. Question 2 of 2 Which patterns match the same texts as ^(\d+)+$ without trying every split of a long row of digits that ends in a letter? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • ^\d++$
    • ^(?>\d+)+$
    • ^\d+$

    Remove nested quantifiers, or make the inner one atomic or possessive; then a failure cannot be retried in exponentially many ways.

Regex needs macOS 13, so set the platform you build for

Regex, its literals and RegexBuilder came with Swift 5.7, and on Apple platforms they need the operating system that ships that runtime: macOS 13, iOS 16 or later, as Apple’s documentation lists. A program built for an older deployment target: The oldest operating-system version a program is built to run on, set with swiftc -target or a package's platforms. Code that uses an API newer than the target, such as Regex below macOS 13, does not compile. does not compile. This session builds alert.swift for macOS 12 and for macOS 13, then tries a lookbehind in both language modes:

lookbehind.swift: a lookbehind assertion Swift · lookbehind.swift
// lookbehind.swift: a lookbehind assertion, which Swift's Regex does not support yet.
let alert = "Balance: 4,500.00"
print(alert.firstMatch(of: #/(?<=Balance: )[\d,.]+/#)?.output ?? "none")
alert.swift for macOS 12 and 13, and a lookbehind in two modes Python · platform_session.py
from session import run, version

# alert.swift built for macOS 12 and for macOS 13, then lookbehind.swift in the Swift 5 and Swift 6 language modes.
version()
run("""
!swiftc -target arm64-apple-macosx12.0 alert.swift -o alert
swiftc -target arm64-apple-macosx13.0 alert.swift -o alert
./alert
!swiftc lookbehind.swift -o lookbehind
!swiftc -swift-version 6 lookbehind.swift -o lookbehind
""", max_lines=8)

Output

# Apple Swift version 6.4 (swiftlang-6.4.0.34.1)
$ swiftc -target arm64-apple-macosx12.0 alert.swift -o alert
alert.swift:3:15: error: 'Regex' is only available in macOS 13.0 or newer
 1 | // alert.swift: one regex literal pulls three facts out of an alert.
 2 | let alert = "Debited 1,250.00 from account XX4321. Status: SUCCESS"
 3 | let pattern = #/(?<amount>\d[\d,]*\.\d\d) from account (?<account>X+\d{4})\. Status: (?<status>[A-Z]+)/#
   |               |- error: 'Regex' is only available in macOS 13.0 or newer
   |               `- note: add 'if #available' version check
 4 | 
 5 | if let match = alert.firstMatch(of: pattern) {
[53 more lines]
[exit status 1]
$ swiftc -target arm64-apple-macosx13.0 alert.swift -o alert
$ ./alert
amount: 1,250.00
account: XX4321
status: SUCCESS
whole match: 1,250.00 from account XX4321. Status: SUCCESS
Regex<(Substring, amount: Substring, account: Substring, status: Substring)>
another alert: firstMatch returned nil
$ swiftc lookbehind.swift -o lookbehind
lookbehind.swift:3:30: error: cannot parse regular expression: lookbehind is not currently supported
1 | // lookbehind.swift: a lookbehind assertion, which Swift's Regex does not support yet.
2 | let alert = "Balance: 4,500.00"
3 | print(alert.firstMatch(of: #/(?<=Balance: )[\d,.]+/#)?.output ?? "none")
  |                              `- error: cannot parse regular expression: lookbehind is not currently supported
4 | 
[exit status 1]
$ swiftc -swift-version 6 lookbehind.swift -o lookbehind
lookbehind.swift:3:30: error: cannot parse regular expression: lookbehind is not currently supported
1 | // lookbehind.swift: a lookbehind assertion, which Swift's Regex does not support yet.
2 | let alert = "Balance: 4,500.00"
3 | print(alert.firstMatch(of: #/(?<=Balance: )[\d,.]+/#)?.output ?? "none")
  |                              `- error: cannot parse regular expression: lookbehind is not currently supported
4 | 
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 platform_session.py

For macOS 12 the compiler stopped at the first Regex with “‘Regex’ is only available in macOS 13.0 or newer”, followed by more lines of the same kind; for macOS 13 the program built and ran. A Swift package that names no platforms gets SwiftPM’s predefined minimum version for each one, which for macOS is far older than 13, so a package that uses Regex declares platforms: [.macOS(.v13)] in Package.swift, or wraps the Regex code in if #available(macOS 13, *) with a fallback for older systems.

The lookbehind (?<=Balance: ) failed too, in both modes: lookbehind assertions are accepted for Swift (SE-0448) but not implemented in Swift 6.4, and the compiler says so. Match the context as plain text and capture only the part you want instead, as #/Balance: (?<amount>[\d,.]+)/# does. Walk through the versions:

Timeline · 4 events in order Regex in Swift, version by version Walk through the versions in order. Each one changed what a regex in Swift can be written as, or where it runs.
  1. 5.7

    Regex arrives new library

    The Regex type (SE-0350), regex literals (SE-0354), RegexBuilder (SE-0351) and the string methods that take a regex (SE-0357). On Apple platforms they need macOS 13 or iOS 16 at run time.

  2. 5.8

    Bare slashes, by choice feature flag

    The upcoming feature BareSlashRegexLiterals lets a project in the Swift 5 language mode write /…/ literals early; #/…/# works without it.

  3. 6.0

    The Swift 6 language mode language mode

    The Swift 6 mode turns /…/ literals on for everyone, and a package with swift-tools-version 6.0 builds in that mode.

  4. 6.4

    Lookbehind still waits not yet

    Lookbehind assertions (SE-0448) are accepted but not implemented: Swift 6.4 refuses (?<=…) with "lookbehind is not currently supported".

The timeline has 4 events, from 5.7 to 6.4.

Checkpoint

Check: OS versions and missing features

  1. Question 1 of 2 A package that uses Regex fails on a build machine with "'Regex' is only available in macOS 13.0 or newer". What is the usual fix?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Declare platforms: [.macOS(.v13)] in Package.swift, or put the Regex code behind if #available(macOS 13, *)

    Without a platforms entry, SwiftPM uses its predefined minimum for macOS, which is older than 13.

  2. Question 2 of 2 You need only the amount from "Balance: 4,500.00". Which way works with Swift 6.4?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Match #/Balance: (?<amount>[\d,.]+)/# and read match.output.amount

    Until SE-0448 is implemented, capture the text you want after its context instead of looking behind for it.

Regex Tester Try a pattern on your own text with live matches and capture groups. It runs JavaScript's regex engine, which differs from Swift's in places: it has lookbehind and no possessive quantifiers.

Practise: alerts in four formats

Write parse(_:), which turns an alert in any of four formats into a transaction: debit or credit, the amount in paise as an Int, the masked account and whether it succeeded. The formats are “Debited … from account XX…. Status: SUCCESS”, “Credited … to account XX…. Status: SUCCESS”, “Debit of … from XX… failed” and “Refund of … credited to XX…”. Anything else returns nil, and so does text longer than 200 Characters, which the function refuses before matching. Then add up the successful debits.

Write and test yours before reading this version:

practice.swift: one way to write it Swift · practice.swift
// practice.swift: parse alerts in four formats into transactions, each pattern matched against the whole text.
struct Transaction {
    let credit: Bool
    let paise: Int
    let account: String
    let ok: Bool
}

typealias AlertPattern = Regex<(Substring, amount: Substring, account: Substring)>

func parse(_ text: String) -> Transaction? {
    guard text.count <= 200 else {
        return nil
    }
    let formats: [(pattern: AlertPattern, credit: Bool, ok: Bool)] = [
        (#/Debited (?<amount>[\d,]+\.\d\d) from account (?<account>XX\d{4})\. Status: SUCCESS/#, false, true),
        (#/Credited (?<amount>[\d,]+\.\d\d) to account (?<account>XX\d{4})\. Status: SUCCESS/#, true, true),
        (#/Debit of (?<amount>[\d,]+\.\d\d) from (?<account>XX\d{4}) failed/#, false, false),
        (#/Refund of (?<amount>[\d,]+\.\d\d) credited to (?<account>XX\d{4})/#, true, true),
    ]
    for format in formats {
        if let match = text.wholeMatch(of: format.pattern), let paise = Int(match.output.amount.filter(\.isNumber)) {
            return Transaction(credit: format.credit, paise: paise, account: String(match.output.account), ok: format.ok)
        }
    }
    return nil
}

let alerts = [
    "Debited 1,250.00 from account XX4321. Status: SUCCESS",
    "Credited 15,000.00 to account XX9876. Status: SUCCESS",
    "Debit of 99.00 from XX4321 failed",
    "Refund of 450.75 credited to XX4321",
    "Debited 1,250.00 from account XX4321. Status: SUCCESS and more",
]
var spent = 0
for alert in alerts {
    guard let t = parse(alert) else {
        print("not recognised:", alert)
        continue
    }
    print(t.credit ? "credit" : "debit", t.paise, "paise", t.account, t.ok ? "ok" : "failed")
    if !t.credit && t.ok {
        spent += t.paise
    }
}
print("spent:", spent, "paise")

Output

debit 125000 paise XX4321 ok
credit 1500000 paise XX9876 ok
debit 9900 paise XX4321 failed
credit 45075 paise XX4321 ok
not recognised: Debited 1,250.00 from account XX4321. Status: SUCCESS and more
spent: 125000 paise

Recorded with Apple Swift 6.4 (swiftlang-6.4.0.34.1) on macOS 26 arm64. To run it yourself: swiftc -O practice.swift -o practice && ./practice

Every pattern has the same named captures, so the four fit in one array of the same Regex type, and the function tries them in turn with wholeMatch. That refused the last alert, which has extra text after a valid one. The amount keeps only its digits, so “15,000.00” becomes 1500000 paise, and the account is copied into a String before it is stored. Close with the practice round. Questions on your shakiest topic come first, and a right answer earns a harder one:

Practice that adapts to you

Practise this lesson

15 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 15 Which regex literal compiles in every Swift language mode?

    Choose one answer.

    Show the answer: Question 1 of 15

    Answer: #/\d+/#

    Write #/…/# in code that may be built in Swift 5 mode.

  2. Question 2 of 15 What does this print?

    Read the code, then choose one answer.

    let pattern = #/(?<coach>[A-Z]\d)-(?<seat>\d+)/#
    if let m = "B2-34".wholeMatch(of: pattern) {
        print(m.output.seat, m.output.coach)
    }
    Show the answer: Question 2 of 15

    Answer: 34 B2

    Named captures become named members of the output tuple, in any order you need.

  3. Question 3 of 15 What does a regex literal give you that try Regex("…") built from a string does not?

    Choose one answer.

    Show the answer: Question 3 of 15

    Answer: The compiler checks the pattern and knows the types of its captures

    Build from a string only when the pattern comes from data; then handle the error it can throw.

  4. Question 4 of 15 What does ChoiceOf { "credited"; "debited" } match?

    Choose one answer.

    Show the answer: Question 4 of 15

    Answer: Either the word credited or the word debited

    ChoiceOf is the builder's |.

  5. Question 5 of 15 When is RegexBuilder a better choice than a literal?

    Choose one answer.

    Show the answer: Question 5 of 15

    Answer: For a long pattern whose parts need names, comments, transforms or reuse

    Mix them freely; a builder can contain literals.

  6. Question 6 of 15 A TryCapture's transform returns nil for the captured text "12a". What does the regex do?

    Choose one answer.

    Show the answer: Question 6 of 15

    Answer: It backtracks and tries other ways to match; if none works, there is no match

    Use TryCapture when a conversion is part of what makes the text valid.

  7. Question 7 of 15 What does this print?

    Read the code, then choose one answer.

    print("4, 16 and 64".matches(of: #/\d+/#).count)
    Show the answer: Question 7 of 15

    Answer: 3

    matches(of:) returns all non-overlapping matches, in order.

  8. Question 8 of 15 What does this print?

    Read the code, then choose one answer.

    print("a ; b;c".split(separator: #/\s*;\s*/#))
    Show the answer: Question 8 of 15

    Answer: ["a", "b", "c"]

    split(separator:) takes a Regex as its separator since SE-0357.

  9. Question 9 of 15 What does "4000011".prefixMatch(of: #/\d{6}/#) find?

    Choose one answer.

    Show the answer: Question 9 of 15

    Answer: A match of 400001, the first six characters, although the text has seven

    firstMatch searches, prefixMatch anchors the start, and only wholeMatch anchors both ends.

  10. Question 10 of 15 What does a backtracking regex engine do when part of a pattern fails?

    Choose one answer.

    Show the answer: Question 10 of 15

    Answer: It goes back to an earlier choice, such as how much a + took, and tries another

    Swift's Regex backtracks, which the TryCapture and Local documentation both describe.

  11. Question 11 of 15 Why does ^(\d+)+$ take so long on twenty 1s followed by an x?

    Choose one answer.

    Show the answer: Question 11 of 15

    Answer: Every way of sharing the digits among the groups is tried and fails, and the number of ways doubles with each digit

    Nested quantifiers over the same characters multiply the ways to fail.

  12. Question 12 of 15 A sign-in endpoint checks user names with ^([a-z]+)*$. What do you change?

    Choose one answer.

    Show the answer: Question 12 of 15

    Answer: Use ^[a-z]{1,32}$ and refuse input longer than the limit before matching

    For patterns that read untrusted text, remove nested quantifiers and bound the input's length, as OWASP advises.

  13. Question 13 of 15 Which macOS version does Swift's Regex need at run time?

    Choose one answer.

    Show the answer: Question 13 of 15

    Answer: macOS 13 or later

    Regex came with Swift 5.7, whose runtime ships with macOS 13 and iOS 16.

  14. Question 14 of 15 An app must still run on macOS 12, but should use Regex where it can. Which code fits?

    Choose one answer.

    Show the answer: Question 14 of 15

    Answer: if #available(macOS 13, *) { use the Regex } else { use a fallback }

    #available makes newer APIs usable in apps that must also run on older systems.

  15. Question 15 of 15 Why does #/(?<=@)\w+/# not compile with Swift 6.4?

    Choose one answer.

    Show the answer: Question 15 of 15

    Answer: Lookbehind assertions are accepted for Swift but not implemented yet, and the compiler refuses them

    Capture the text after its context instead, until lookbehind is implemented.

Interview questions

Warm-up: what is the difference between firstMatch(of:) and wholeMatch(of:) in Swift?

firstMatch(of:) searches: it tries the pattern at each position of the string from the start and returns the first match it finds, or nil, so "Ref PNR442177".firstMatch(of: #/[A-Z]{3}\d{6}/#) finds PNR442177 in the middle. wholeMatch(of:) succeeds only when the pattern matches the entire string, from its first Character to its last, so the same call returns nil, because “Ref “ is in the way. Use firstMatch or matches(of:) to extract data from longer text, and wholeMatch to validate a value such as a booking reference or a code, since a validator that accepts a valid piece inside invalid text accepts invalid input. prefixMatch(of:) sits between them: the match must start at the beginning but may end anywhere. All three return a match whose output holds the typed captures.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A service you maintain validates user names with the pattern ^([a-z0-9]+)*$. A request with a 30-character name followed by an exclamation mark keeps one CPU busy for minutes. Explain what the regex engine is doing, how you would show it safely, and how you would fix the validator.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    You move the alert-parsing code of a macOS 14 app, which uses Regex, into a new Swift package. The package fails to build with "'Regex' is only available in macOS 13.0 or newer". Why, how do you fix it, and what changes if the same package must also serve an app that supports macOS 12?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A regex literal, #/…/# in any language mode or /…/ in Swift 6 mode, is checked by the compiler, and its named captures become typed parts of the match’s output.
  • RegexBuilder writes patterns as Swift code with Capture, Repeat, ChoiceOf and CharacterClass, and TryCapture converts a capture, such as an amount into an Int, failing the match when it cannot.
  • String methods take a Regex: firstMatch and matches find, replacing and split change and cut, and wholeMatch is the test for validating a whole value.
  • Nested quantifiers such as (\d+)+ can make the work double with every character; flatten them or use an atomic group, and bound the length of untrusted input.
  • Regex needs macOS 13 or iOS 16, so a package declares its platforms, and lookbehind is not available in Swift 6.4.

Terms in this lesson

atomic group
A part of a regex, (?>…) or RegexBuilder's Local, that keeps the first match it finds and drops its other choices, so the engine cannot retry it in other ways after a later failure.
backtracking
What a regex engine does when part of a pattern fails: it returns to an earlier choice, such as how many characters a + took, and tries another. Nested quantifiers can make the choices grow exponentially.
capture
A part of a regex in brackets, (…) or (?<name>…), whose matched text is returned in the match's output. Swift types it as a Substring, or as the value a TryCapture transform returns.
deployment target
The oldest operating-system version a program is built to run on, set with swiftc -target or a package's platforms. Code that uses an API newer than the target, such as Regex below macOS 13, does not compile.
regular expression
A pattern that describes the text it matches, such as [A-Z]{3}\d{6} for three capital letters and six digits. Swift's Regex type matches one against a string and returns the parts it captured.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.