Skip to content

Swift Regex

Processing strings to extract, manipulate, or search data is a core skill that most software engineers need to acquire.

Regular expressions, also known as regex, are powerful tools for working with strings.

Swift Regex brings first-class support for regular expressions to the Swift language.

The Swift compiler natively supports regex syntax, which gives us compile time errors, syntax highlighting, and strongly typed captures.

Create a Regex

You can create a Regex instance using regular expression syntax, either in a regex literal or a string.

Compile Time Regex

swift
// creating a Regex using a regex literal
let keyAndValue = /(.+?): (.+)/
// creating a Regex using a regex literal
let keyAndValue = /(.+?): (.+)/

This regular expression is a first-class type in Swift and can be assigned directly to a variable.

As a Swift type, Xcode will also recognize this regex and provide both compile time checks and syntax highlighting.

Runtime Regex

Swift Regex also supports creating regular expressions at runtime.

swift
// creating a Regex from a pattern in a string
let simpleDigits = try Regex("[0-9]+")
// creating a Regex from a pattern in a string
let simpleDigits = try Regex("[0-9]+")

Here the regular expression is created by constructing the Regex type with a String representing the regular expression. The try keyword is used since a Regex can throw an error if the supplied regular expression is invalid.

Regex Builder

The new Regex Builder introduces a declarative approach to composing regular expressions.

swift
import RegexBuilder
let regex = Regex {
    One(.digit)
}
import RegexBuilder
let regex = Regex {
    One(.digit)
}

Check for the presence of a pattern

Calling contains(_:) to check for the presence of a pattern

swift
let simpleDigits = try Regex("[0-9]+")
let setting = "color: 161 103 230"
if setting.contains(simpleDigits) {
    print("'\(setting)' contains some digits.")
}
// Prints "'color: 161 103 230' contains some digits."
let simpleDigits = try Regex("[0-9]+")
let setting = "color: 161 103 230"
if setting.contains(simpleDigits) {
    print("'\(setting)' contains some digits.")
}
// Prints "'color: 161 103 230' contains some digits."

Search for a pattern in a string

When you find a match, the resulting Regex.Match type includes an output property that contains the matched substring along with any captures.

firstMatch(of:)

The firstMatch(of:) method returns the first match for the regex within this collection

swift
let keyAndValue = /(.+?): (.+)/
let setting = "color: 161 103 230"
if let match = setting.firstMatch(of: keyAndValue) {
    print("Key: \(match.1)")
    print("Value: \(match.2)")
}
// Key: color
// Value: 161 103 230
let keyAndValue = /(.+?): (.+)/
let setting = "color: 161 103 230"
if let match = setting.firstMatch(of: keyAndValue) {
    print("Key: \(match.1)")
    print("Value: \(match.2)")
}
// Key: color
// Value: 161 103 230

The ? in the (.+?) part means lazy mode.

matches(of:)

The matches(of:) method returns a collection containing all non-overlapping matches of the regex.

wholeMatch(of:)

The wholeMatch(of:) method matches a regex in its entirety.

swift
let search1 = /My name is (.+?) and I'm (\d+) years old./
let greeting1 = "My name is Taylor and I'm 26 years old."

if let result = try? search1.wholeMatch(in: greeting1) {
    print("Name: \(result.1)")
    print("Age: \(result.2)")
}
let search1 = /My name is (.+?) and I'm (\d+) years old./
let greeting1 = "My name is Taylor and I'm 26 years old."

if let result = try? search1.wholeMatch(in: greeting1) {
    print("Name: \(result.1)")
    print("Age: \(result.2)")
}

That creates a regex looking for two particular values in some text, and if it finds them both prints them. But notice how the result tuple can reference its matches as .1 and .2, because Swift knows exactly which matches will occur. (In case you were wondering, .0 will return the whole matched string.)

swift
let search2 = /My name is (?<name>.+?) and I'm (?<age>\d+) years old./
let greeting2 = "My name is Taylor and I'm 26 years old."

if let result = try? search2.wholeMatch(in: greeting2) {
    print("Name: \(result.name)")
    print("Age: \(result.age)")
}
let search2 = /My name is (?<name>.+?) and I'm (?<age>\d+) years old./
let greeting2 = "My name is Taylor and I'm 26 years old."

if let result = try? search2.wholeMatch(in: greeting2) {
    print("Name: \(result.name)")
    print("Age: \(result.age)")
}

Call this method if you want the regular expression to succeed only when it matches the entire string you pass as string.

swift
let digits = /
/


if let digitsMatch = try digits.wholeMatch(in: "2022") {
    print(digitsMatch.0)
} else {
    print("No match.")
}
// Prints "2022"


if let digitsMatch = try digits.wholeMatch(in: "The year is 2022.") {
    print(digitsMatch.0)
} else {
    print("No match.")
}
// Prints "No match."
let digits = /
/


if let digitsMatch = try digits.wholeMatch(in: "2022") {
    print(digitsMatch.0)
} else {
    print("No match.")
}
// Prints "2022"


if let digitsMatch = try digits.wholeMatch(in: "The year is 2022.") {
    print(digitsMatch.0)
} else {
    print("No match.")
}
// Prints "No match."

prefixMatch(of:):

The prefixMatch(of:) returns a match if this string is matched by the given regex at its start.

In fact, we can go even further because regular expressions allow us to name our matches, and these flow through to the resulting tuple of matches:

Parse top command with RegexBuilder

shell
top -l 1 -o mem -n 8 -stats pid,command,pstate,mem | sed 1,12d
top -l 1 -o mem -n 8 -stats pid,command,pstate,mem | sed 1,12d

The command above outputs:

18173* Xcode            sleeping 4667M
41019* com.apple.WebKit sleeping 4034M
29685* lldb-rpc-server  sleeping 3285M
395*   WindowServer     sleeping 1322M
18360* XCBBuildService  sleeping 1058M
1008*  Google Chrome He sleeping 986M
18203* com.apple.dt.SKA sleeping 801M
70744* Google Chrome He sleeping 792M
18173* Xcode            sleeping 4667M
41019* com.apple.WebKit sleeping 4034M
29685* lldb-rpc-server  sleeping 3285M
395*   WindowServer     sleeping 1322M
18360* XCBBuildService  sleeping 1058M
1008*  Google Chrome He sleeping 986M
18203* com.apple.dt.SKA sleeping 801M
70744* Google Chrome He sleeping 792M

Pase output above with RegexBuilder

swift
import RegexBuilder

let top = """
18173* Xcode            sleeping 4667M
41019* com.apple.WebKit sleeping 4034M
29685* lldb-rpc-server  sleeping 3285M
395*   WindowServer     sleeping 1322M
18360* XCBBuildService  sleeping 1058M
1008*  Google Chrome He sleeping 986M
18203* com.apple.dt.SKA sleeping 801M
70744* Google Chrome He sleeping 792M
"""

let separator = /\s{1,}/
let topMatcher = Regex {
    OneOrMore(.digit)
    ZeroOrMore("*")
    separator
    Capture(
        OneOrMore(.any, .reluctant)
    )
    separator
    Capture(
        ChoiceOf {
            "running"
            "sleeping"
            "stuck"
            "idle"
            "stopped"
            "halted"
            "zombie"
            "unknown"
        }
    )
    separator
    Capture {
        OneOrMore(.digit)
        // /M|K|B/
        ChoiceOf {
            "M"
            "K"
            "B"
        }
        Optionally(/\+|-/)
    }
}
// 8
let matches = top.matches(of: topMatcher)
for match in matches {
    // 9
    let (_, name, status, size) = match.output
    print("\(name) \t\t \(status) \t\t \(size)")
}
import RegexBuilder

let top = """
18173* Xcode            sleeping 4667M
41019* com.apple.WebKit sleeping 4034M
29685* lldb-rpc-server  sleeping 3285M
395*   WindowServer     sleeping 1322M
18360* XCBBuildService  sleeping 1058M
1008*  Google Chrome He sleeping 986M
18203* com.apple.dt.SKA sleeping 801M
70744* Google Chrome He sleeping 792M
"""

let separator = /\s{1,}/
let topMatcher = Regex {
    OneOrMore(.digit)
    ZeroOrMore("*")
    separator
    Capture(
        OneOrMore(.any, .reluctant)
    )
    separator
    Capture(
        ChoiceOf {
            "running"
            "sleeping"
            "stuck"
            "idle"
            "stopped"
            "halted"
            "zombie"
            "unknown"
        }
    )
    separator
    Capture {
        OneOrMore(.digit)
        // /M|K|B/
        ChoiceOf {
            "M"
            "K"
            "B"
        }
        Optionally(/\+|-/)
    }
}
// 8
let matches = top.matches(of: topMatcher)
for match in matches {
    // 9
    let (_, name, status, size) = match.output
    print("\(name) \t\t \(status) \t\t \(size)")
}

The code above gives us the following output:

Xcode 		 sleeping 		 4667M
com.apple.WebKit 		 sleeping 		 4034M
lldb-rpc-server 		 sleeping 		 3285M
WindowServer 		 sleeping 		 1322M
XCBBuildService 		 sleeping 		 1058M
Google Chrome He 		 sleeping 		 986M
com.apple.dt.SKA 		 sleeping 		 801M
Google Chrome He 		 sleeping 		 792M
Xcode 		 sleeping 		 4667M
com.apple.WebKit 		 sleeping 		 4034M
lldb-rpc-server 		 sleeping 		 3285M
WindowServer 		 sleeping 		 1322M
XCBBuildService 		 sleeping 		 1058M
Google Chrome He 		 sleeping 		 986M
com.apple.dt.SKA 		 sleeping 		 801M
Google Chrome He 		 sleeping 		 792M

References