regexpr
regexpr(pattern, text, ignore.case = FALSE, perl = FALSE, fixed = FALSE, useBytes = FALSE) regexpr() finds the first regex match in each element of a character vector and returns the starting position together with the matched length. It belongs to the grep family in base R and is the function to reach for when you need match positions, not just a yes/no answer. The result pairs cleanly with regmatches() to pull the actual matched text back out.
Syntax
regexpr(pattern, text, ignore.case = FALSE, perl = FALSE,
fixed = FALSE, useBytes = FALSE)
The second argument is named text, not x, and that naming is what distinguishes regexpr() from grep(), grepl(), sub(), and gsub(). The two required arguments (pattern and text) can be passed positionally, but everything after text should be passed by name.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
pattern | character | — | Regex pattern. NA is an error for regexpr(). |
text | character | — | Character vector to search. Long vectors supported. |
ignore.case | logical | FALSE | Case-insensitive matching when TRUE. |
perl | logical | FALSE | Use PCRE (Perl-compatible regex). Required for named captures. |
fixed | logical | FALSE | Treat pattern as a literal string. Skips regex compilation. |
useBytes | logical | FALSE | Match byte-by-byte; positions and lengths become byte counts. |
Pass ignore.case, perl, fixed, and useBytes by name. Positional args after text trigger “partial argument match” warnings on R 4.0+.
Return value
regexpr() returns an integer vector the same length as text giving the 1-based starting position of the first match in each element, or -1 if there is no match. The vector carries a match.length attribute (also integer, -1 for no match) holding the matched length in characters, or in bytes when useBytes = TRUE.
When the pattern contains parenthesised groups, three more attributes appear on the result:
capture.start— integer matrix with one column per group, giving the start position of each capture.capture.length— integer matrix with the length of each capture.capture.names— character vector of group names, present only when groups are named (which requiresperl = TRUE).
Because the attributes are how regexpr() reports everything beyond the position, the canonical way to get the matched substring is regmatches(text, m). To make the attribute shape concrete, here is a minimal call and the attributes you can read off the result:
m <- regexpr("a+", c("banana", "bbb", NA))
m
# [1] 2 -1 NA
attr(m, "match.length")
# [1] 1 -1 NA
The position and match.length vectors are always the same length, and they line up element-wise, so you can index one with the other without checking shapes first.
Examples
First match position and length
m <- regexpr("stat", c("statistics", "estate", "status", "tact"))
m
# [1] 1 3 1 -1
attr(m, "match.length")
# [1] 4 4 4 -1
The -1 for "tact" means no match, and that is the value you test for when checking misses. attr(m, "match.length") carries the same shape, with -1 lining up position-wise with each non-match, so you can index both vectors together safely.
Pull the matched text with regmatches()
fruits <- c("apple", "banana", "cherry", "date", "elderberry")
m <- regexpr("a", fruits)
regmatches(fruits, m)
# [1] "a" "a" "a"
regmatches() reads the position and length off m and slices the input for you. Elements that did not match come back as character(0), which is easy to forget when you later try to bind results back into a data frame; those slots have zero length rather than NA, so c() and data.frame() will silently drop them.
Manual extraction with substring math
If you do not want regmatches(), compute the end index yourself and use substring():
words <- c("foo123bar", "baz", "qux456")
m <- regexpr("[0-9]+", words)
mlen <- attr(m, "match.length")
ends <- ifelse(m > 0, m + mlen - 1L, NA_integer_)
substring(words, m, ends)
# [1] "123" NA "456"
The ifelse() guard avoids underflowing 0 + (-1) - 1 on the no-match entry, which would otherwise wrap to a huge index and pull garbage out of the input. Note that NA_integer_ in the result preserves the original NA from the input, so the underflow only happens for the literal -1 miss case.
Case-insensitive matching
regexpr("R", c("R tutorial", "R-base", "rstats"), ignore.case = TRUE)
# [1] 1 1 1
The third element matches because ignore.case = TRUE lets the lowercase r in rstats count as R. Without the flag, only the first two elements would match at position 1. The flag also combines cleanly with perl = TRUE, which is the usual way to get case-insensitive PCRE matching with named captures.
Fixed-string mode
When the pattern has no metacharacters, pass fixed = TRUE to skip regex compilation. The flag short-circuits the pattern engine entirely, so a literal lookup runs faster and you avoid the risk of accidental metacharacter interpretation, for instance a . in a file path becoming a wildcard:
paths <- c("/usr/local/bin/R", "C:\\R\\bin", "/opt/R-4.4")
regexpr("R", paths, fixed = TRUE)
# [1] 12 4 6
Named capture groups
Named captures require perl = TRUE, which switches the engine to PCRE. The group positions and lengths land on the capture.* attributes and can be sliced out with regmatches() if you pass the m object directly:
s <- c("John: 42", "Alice: 7", "nobody here")
m <- regexpr("(?<name>\\w+): (?<n>\\d+)", s, perl = TRUE)
m
# [1] 1 1 -1
attr(m, "capture.start")
# name n
# [1,] 1 7
# [2,] 1 8
# [3,] -1 -1
attr(m, "capture.names")
# [1] "name" "n"
NA handling
NA in text produces an NA position and NA length, which means downstream code needs to handle both NA and the literal -1 miss separately. NA here means the input itself was missing; -1 means the regex did not match. Do not collapse the two cases:
regexpr("a", c("banana", NA, "apple"))
# [1] 2 NA 1
# attr(., "match.length") -> 1 NA 1
NA in pattern is an error for regexpr() and gregexpr(), even though the same input is permitted by grep() and grepl().
Common mistakes
Checking for no match with is.na(). A miss returns -1, not NA. Use m == -1 (and pair it with attr(m, "match.length") == -1 when you also need the length).
Confusing characters and bytes. Positions and lengths are in characters by default. With useBytes = TRUE, they switch to bytes. This matters for multibyte text such as accented characters or CJK. The simplest way to see the difference is to compare the two modes on the same UTF-8 string:
# 'café' is 4 characters but 5 bytes because é is two-byte UTF-8.
regexpr("é", "café")
# [1] 4
# attr(., "match.length") -> 1
regexpr("é", "café", useBytes = TRUE)
# [1] 5
# attr(., "match.length") -> 2
A naïve substring(text, m, m + attr(m, "match.length") - 1) call that ignores the mode flag will slice the wrong characters on a multibyte input.
Reaching for regexpr() when you want all matches. It returns only the first match per element. Use gregexpr() for every disjoint match.
Using POSIX word boundaries. ?grep warns that POSIX 1003.2 mode in gsub() and gregexpr() misbehaves on repeated \b. Switch to perl = TRUE for word-boundary work.
Reaching for regexpr() when a literal lookup would do. If pattern has no metacharacters and you only need a yes/no answer, fixed = TRUE is faster and removes any risk of accidental regex interpretation.
See also
- grep() — Returns element indices, not match positions; useful when you want to subset a vector, not slice into each element.
- gsub() — Replaces every match instead of returning positions; pairs with
regexpr()when you also want to inspect matches before replacing them. - Regular expressions in R — A wider guide to regex syntax, greedy vs. lazy quantifiers, and lookarounds in base R.