Regular Expressions

Reviewed & published by Brayan K

A regular expression is a compact pattern language for describing text, and in R you use it with functions like grepl() and gsub() to detect, extract, and replace pieces of strings.

Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

This lesson covers detecting matches with grepl/grep, replacing with sub/gsub, extracting with regmatches, the core anchors, classes and quantifiers, and R's two-backslash escaping along with fixed= and perl= modes.

What You'll Learn in This Lesson

1️⃣ Detecting Matches: grepl() & grep()

grepl() returns TRUE/FALSE for each string — ideal for filtering. grep() returns the matching positions, or the matching strings with value = TRUE. The anchors ^ (start) and $ (end) pin the pattern to the edges.

# grepl() asks "does each string MATCH the pattern?" -> TRUE/FALSE
words <- c("apple", "banana", "cherry", "grape")

grepl("an", words)      # contains "an"? -> [1] FALSE  TRUE FALSE FALSE
grepl("e$", words)      # ENDS in "e"?   -> [1]  TRUE FALSE FALSE  TRUE
grepl("^a", words)      # STARTS with a? -> [1]  TRUE FALSE FALSE FALSE

# grep() returns the POSITIONS that match (value = TRUE gives the strings)
grep("e$", words)              # -> [1] 1 4
grep("e$", words, value = TRUE)  # -> [1] "apple" "grape"

2️⃣ Replacing: sub() & gsub()

sub() changes the first match; gsub() changes them all (g for "global"). Character classes like [0-9] match any digit and the quantifier + means "one or more". Note the whitespace shorthand \s — in an R string you write it with two backslashes.

# sub() replaces the FIRST match; gsub() replaces ALL matches.
sub("o", "0", "foo boo")    # first o only -> [1] "f0o boo"
gsub("o", "0", "foo boo")   # every o      -> [1] "f00 b00"

# Character classes: [0-9] a digit, [a-z] a letter. + means "one or more".
gsub("[0-9]+", "#", "Room 101, Seat 7")  # -> [1] "Room #, Seat #"

# Whitespace class \\s (written with TWO backslashes in an R string):
gsub("\\s+", "_", "a  b   c")           # -> [1] "a_b_c"

3️⃣ Extracting Matches

To pull the matched text out (rather than replace it), pair a locator with regmatches(). regexpr() finds the first match; gregexpr() finds them all and regmatches() returns them as a list.

# Extracting matches: regexpr() finds WHERE, regmatches() pulls it OUT.
text <- "Order 4471 shipped"
m <- regexpr("[0-9]+", text)   # locate the first run of digits
regmatches(text, m)            # extract it -> [1] "4471"

# Use gregexpr() + regmatches() to get ALL matches as a list:
s <- "cats=3 dogs=12 fish=5"
mm <- gregexpr("[0-9]+", s)
regmatches(s, mm)              # -> [[1]]
                              #    [1] "3"  "12" "5"

4️⃣ Escaping, fixed=, and perl=

Regex metacharacters like . need escaping to match literally — and in an R string the backslash itself is doubled, so the regex \. becomes "\\.". To skip regex entirely, pass fixed = TRUE; for advanced features, pass perl = TRUE.

# In an R STRING you type a backslash twice. So the regex \\. (an
# escaped literal dot) is written "\\." in your R code.
gsub("\\.", "_", "file.name.txt")   # escape the dots -> [1] "file_name_txt"

# fixed = TRUE turns OFF regex — the pattern is taken literally.
# Handy when your text contains special characters like . or ( ).
gsub(".", "_", "a.b.c", fixed = TRUE) # only real dots -> [1] "a_b_c"

# perl = TRUE enables Perl-style features (lookarounds, \\d, etc.)
grepl("\\d{3}", "abc123", perl = TRUE)  # three digits? -> [1] TRUE

Your turn. Fill in the ___ blanks, run it, and compare with the expected output.

# 🎯 YOUR TURN — replace each ___ then run it.
emails <- c("ann@x.com", "bob_at_x.com", "cy@y.org")

# 1) Which strings look like an email (contain an @)?
print(grepl("___", emails))     # 👉 replace ___ with @

# 2) Mask every digit in this code with gsub()
print(gsub("[0-9]", "*", "PIN 4827"))

# 3) Keep only the strings that END in ".com"
print(grep("\\.com$", emails, value = TRUE))

# ✅ Expected output:
# [1]  TRUE FALSE  TRUE
# [1] "PIN ****"
# [1] "ann@x.com"    "bob_at_x.com"

Mini-Challenge: Tidy Phone Labels

The key trick here is the negated class [^0-9-] — "anything that is NOT a digit or a dash" — which is the cleanest way to strip a string down to just the characters you want to keep.

# 🎯 MINI-CHALLENGE: tidy a messy list of phone labels.
#
# raw <- c("Tel: 555-0100", "Tel: 555-0199", "n/a", "Tel: 555-0123")
#
# 1. Use grepl("^Tel:") to flag the rows that have a number.
# 2. Use gsub("[^0-9-]", "") to strip everything that ISN'T a digit
#    or a dash, leaving just the phone number.
# 3. Use grep(..., value = TRUE) to return only the real entries.
# 4. Count how many are "n/a" with sum(raw == "n/a").
#
# ✅ Example output:
#    [1]  TRUE  TRUE FALSE  TRUE
#    [1] "555-0100" "555-0199" ""         "555-0123"
#    [1] "Tel: 555-0100" "Tel: 555-0199" "Tel: 555-0123"
#    [1] 1

# your code here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Regex in R

TaskR Syntax
Does it match? (logical)grepl("pat", x)
Matching stringsgrep("pat", x, value = TRUE)
Replace allgsub("a", "b", x)
Extract first matchregmatches(x, regexpr("[0-9]+", x))
Literal textgsub(".", "_", x, fixed = TRUE)
A digit / one or more"\\d" / "+"

🎉 Lesson Complete!

Practice quiz

What does grepl() return?

  • The matching strings
  • A logical TRUE/FALSE for each element
  • The match positions
  • The replaced text

Answer: A logical TRUE/FALSE for each element. grepl() returns a logical vector; the 'l' stands for logical.

What does grep('e$', x) return?

  • The integer positions that match
  • TRUE/FALSE per element
  • Only the first match
  • A list of matches

Answer: The integer positions that match. grep() returns the positions; add value = TRUE to get the strings.

Which function replaces ALL matches in each string?

  • sub()
  • replace()
  • gsub()
  • regmatches()

Answer: gsub(). gsub() replaces every match; the g is for global. sub() replaces only the first.

In an R string, how do you write the regex for a digit, \d?

  • "d"
  • "\d"
  • "//d"
  • "\\d"

Answer: "\\d". R's parser needs the backslash doubled, so \d is written "\\d".

Which anchor matches the END of a string?

  • $
  • ^
  • .
  • *

Answer: $. $ anchors to the end; ^ anchors to the start.

What does the quantifier + mean?

  • Zero or more
  • One or more
  • Exactly one
  • Optional

Answer: One or more. + means one or more of the preceding element.

What does fixed = TRUE do?

  • Enables Perl features
  • Makes matching case-insensitive
  • Turns regex off so the pattern is literal
  • Anchors the pattern

Answer: Turns regex off so the pattern is literal. fixed = TRUE matches the pattern literally, so . means a real dot.

Which pair extracts the matched text rather than replacing it?

  • regexpr() + regmatches()
  • sub() + gsub()
  • grepl() + grep()
  • paste() + cat()

Answer: regexpr() + regmatches(). regexpr() locates the match and regmatches() pulls it out.

What does the character class [0-9] match?

  • Any letter
  • Any single digit
  • Whitespace
  • Any character

Answer: Any single digit. [0-9] matches a single digit character.

What does perl = TRUE enable?

  • Literal matching
  • Case folding
  • Slower matching only
  • Perl-style features like lookarounds and \d

Answer: Perl-style features like lookarounds and \d. perl = TRUE switches to the PCRE engine with richer features.

Continue this course

Frequently asked questions

What is the difference between grepl() and grep()?

Both search a character vector for a pattern, but they return different things. grepl() returns a logical vector — TRUE or FALSE for each element — which is perfect for filtering with x[grepl(pattern, x)] or counting with sum(). grep() returns the integer positions of the elements that matched, and with value = TRUE it returns the matching strings themselves. A simple memory aid: grepl ends in 'l' for logical, grep gives locations.

When do I use sub() versus gsub()?

sub() replaces only the FIRST match in each string, while gsub() (the 'g' is for global) replaces EVERY match. So sub("o", "0", "foo") gives "f0o" but gsub("o", "0", "foo") gives "f00". For cleaning data you almost always want gsub() because you want to fix all occurrences; reach for sub() only when you specifically need to change just the first one, such as splitting on the first delimiter.

Why do I need two backslashes in R regex patterns?

There are two layers of escaping. First R's string parser reads your text, where a backslash starts an escape sequence, so to get a single literal backslash into the string you must type two: "\\". Then the regex engine receives that single backslash and interprets the sequence, so the regex \d (a digit) must be written "\\d" in R code, and an escaped literal dot \. is written "\\.". If you ever see 'Trailing backslash' or 'unrecognized escape' errors, you almost certainly need to double a backslash.

What do fixed = TRUE and perl = TRUE do?

fixed = TRUE turns regex OFF entirely: the pattern is matched literally, so a dot means a real dot and parentheses mean real parentheses. It is the easy, safe way to find or replace text that happens to contain regex metacharacters. perl = TRUE switches to the PCRE (Perl-compatible) engine, which supports richer features like lookahead/lookbehind, \d / \w shorthand, and non-greedy quantifiers. Use fixed for literal text, the default engine for everyday patterns, and perl when you need advanced constructs.