
seekr turns search-and-replace into an inspectable R
workflow.
Instead of treating search, replacement, and file modification as a
single operation, seekr separates them into explicit steps.
You can decide which files to search, which files to exclude, inspect
which files were excluded, what matches were found, filter them, stage
or update replacements, and only then write the selected changes to
disk.
At the center of this workflow is the seekr_match
vector: a structured object where each element represents one
independent match in one file, together with its location, matched text,
optional replacement, and surrounding context. After matching, most
operations (summarizing, printing, filtering, and replacing) work
directly with this object.
seekr_match is designed to behave like a vector of
matches while storing the fields needed to inspect, filter, update, and
replace them safely. The design
choices article explains why seekr uses this
representation and how it makes safe replacement possible.
In real projects, search-and-replace often raises questions that are difficult to answer: Which files were considered? Which files were excluded, and why? Which matches were found? Which replacements will be applied? Can I keep only some matches? Can I restore the previous files if needed?
seekr provides a set of functions that make this
workflow explicit, composable, and safe:
list_files(). Start
from one or more directories, recurse into subdirectories, optionally
restrict discovery with Git, and get a normalized character vector of
file paths.filter_files(). Keep
files by extension, path pattern, or size, and use a sensible default
set of exclude functions to remove files that should not be
searched.exclusions(). Inspect which files were excluded and why,
instead of silently excluding files by mistake.match_files(), or
use seek() to combine listing, filtering, and matching in
one call. Replacements can be literal strings, backreferences,
functions, or functions that operate on the capture group matrix.summary(). Get
a compact overview of the number of matches, their distribution by file
and extension, and the frequency of each match/replacement pair.print(). Inspect
matches with surrounding context, preview replacements, and use rich
terminal output with clickable OSC8 links when supported.filter_match().
Keep or discard matches after searching, without running the search
again.field(). Search first, inspect the result, then update the
replacement field to decide what each selected match should
become before writing files.replace_files(). Starting from a seekr_match
vector, replace_files() checks that each affected file
still has the same text that was searched, then replaces only the
matches still present in the vector with their corresponding
replacements.list_backups(), last_backup(),
restore_files(), and
restore_files_interactive(). Review automatic backups and
recover previous file contents if something did not go as expected.For more advanced workflows, a seekr_match vector can
also be converted to a tibble with as_tibble() and
converted back with as_match(). This can make it easier to
create custom summaries, filter matches, or prepare replacements with
grouped operations. See the tabular
workflows article for a more detailed example.
If your text does not come directly from files, or if you want to control reading and writing yourself, see working with text article.
For larger repositories or performance-sensitive searches, see the performance notes article.
Patterns are powered by stringr and ICU regular
expressions, so you can use familiar tools such as
stringr::regex() and stringr::fixed() when you
need more control.
# Install the package from CRAN:
install.packages("seekr")The following example uses the example files shipped with
seekr.
In a simple workflow, you can search for a pattern, inspect or filter the matches, and then apply only the selected replacements.
For example, this finds "foo" in R files listed
recursively from the working directory, prepares "bar" as
the replacement, excludes matches from files whose path contains
"test", and then applies the selected replacements to the
files.
matches <- seek("foo", "bar", extension = "R")
filtered <- filter_match(matches, !grepl("test", path))
replaced <- replace_files(filtered)
seekr("foo", "bar") |>
filter_match(!grepl("test", path)) |>
replace_files()The sections below unpack this workflow step by step: how files are
listed and filtered, how matches and replacements are stored in a
seekr_match vector, how matches can be inspected or
updated, and how replacements are finally written to disk.
First, list all files that could be searched.
files <- list_files()
files
#> [1] "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/config.yaml"
#> [2] "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/data.json"
#> [3] "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/iris.csv"
#> [4] "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/mtcars.csv"
#> [5] "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/script1.R"
#> [6] "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/script2.R"
#> [7] "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/server1.log"
#> [8] "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/server2.log"Then filter to keep only R files. filter_files() records
which files were excluded and why. The exclusions attribute
can be retrieved using exclusions().
filtered <- filter_files(files, extension = "R")
filtered
#> [1] "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/script1.R"
#> [2] "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/script2.R"
#> attr(,"exclusions")
#> # A tibble: 8 × 7
#> path excluded exclude_by_extension is_git_dir is_dependency_dir is_minified_file is_not_text_mime
#> <chr> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl>
#> 1 C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-… TRUE TRUE NA NA NA NA
#> 2 C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-… TRUE TRUE NA NA NA NA
#> 3 C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-… TRUE TRUE NA NA NA NA
#> 4 C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-… TRUE TRUE NA NA NA NA
#> 5 C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-… FALSE FALSE FALSE FALSE FALSE FALSE
#> 6 C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-… FALSE FALSE FALSE FALSE FALSE FALSE
#> 7 C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-… TRUE TRUE NA NA NA NA
#> 8 C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-… TRUE TRUE NA NA NA NANow that we have a list of files, we can search for function names composed of two words separated by an underscore and prepare a replacement that reverses them.
my_pattern <- "([a-z]+)_([a-z]+)(?= <- function)"
my_replacement <- "\\2_\\1"
x <- match_files(filtered, my_pattern, my_replacement)x is a seekr_match vector. It behaves like
a vector of matches, but each match also stores fields that can be
inspected with fields() and accessed with
field().
str(x)
#> <seekr::match[5]> vctrs::rcrd
#> path <chr> "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata/script1.R", "C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/…
#> start_line <int> 1, 9, 2, 7, 12
#> end_line <int> 1, 9, 2, 7, 12
#> start <int> 1, 115, 33, 125, 213
#> end <int> 7, 123, 41, 131, 224
#> start_col <int> 1, 1, 1, 1, 1
#> end_col <int> 7, 9, 9, 7, 12
#> match <chr> "add_one", "say_hello", "mean_safe", "sd_safe", "print_vector"
#> replacement <chr> "one_add", "hello_say", "safe_mean", "safe_sd", "vector_print"
#> before <chr> NA, "\r\ncapitalize <- function(txt) {\r\n toupper(substr(txt, 1, 1))\r\n}\r\n", "# TODO: optimize this function", "mean_safe <- fu…
#> line <chr> "add_one <- function(x) {", "say_hello <- function(name) {", "mean_safe <- function(x) {", "sd_safe <- function(x) {", "print_vector…
#> after <chr> " return(x + 1)\r\n}\r\n\r\ncapitalize <- function(txt) {\r\n toupper(substr(txt, 1, 1))", " paste('Hello', name)\r\n}\r\n", " i…
#> encoding <chr> "UTF-8", "UTF-8", "UTF-8", "UTF-8", "UTF-8"
#> hash <chr> "6861824a9a14bce8180144e4716e3b6d", "6861824a9a14bce8180144e4716e3b6d", "936f5a0f99aca61483471cfc5223e8d6", "936f5a0f99aca61483471cf…
fields(x)
#> [1] "path" "start_line" "end_line" "start" "end" "start_col" "end_col" "match" "replacement" "before"
#> [11] "line" "after" "encoding" "hash"
field(x, "match")
#> [1] "add_one" "say_hello" "mean_safe" "sd_safe" "print_vector"
field(x, "replacement")
#> [1] "one_add" "hello_say" "safe_mean" "safe_sd" "vector_print"Use summary() to get a compact overview of the matches
and planned replacements.
summary(x)
#> ── <seekr::match[5]> ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#> Common Path: C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata
#>
#> Top sources [2]
#> • script2.R : 3 (60.0%)
#> • script1.R : 2 (40.0%)
#>
#> Top matches/replacements [5]
#> • <say_hello/hello_say> : 1 (20.0%)
#> • <add_one/one_add> : 1 (20.0%)
#> • <mean_safe/safe_mean> : 1 (20.0%)
#> • <sd_safe/safe_sd> : 1 (20.0%)
#> • <print_vector/vector_print> : 1 (20.0%)
#>
#> Top extension [1]
#> • r : 5 (100.0%)
#>
#> Top encoding [1]
#> • UTF-8 : 5 (100.0%)Use print() to inspect each match with surrounding
context and preview the replacement. In terminals that support OSC8
hyperlinks, file locations are printed as clickable links, so you can
jump directly from the console to the start of the match.
print(x, context = c(2, 1))
#> <seekr::match[5]> 2 sources
#> Common Path: C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -- 1 | add_one <- function(x) {
#> ++ 1 | one_add <- function(x) {
#> 2 | return(x + 1)
#>
#> 7 | }
#> 8 |
#> [2] -- 9 | say_hello <- function(name) {
#> ++ 9 | hello_say <- function(name) {
#> 10 | paste('Hello', name)
#>
#> script2.R [3]
#> 1 | # TODO: optimize this function
#> [3] -- 2 | mean_safe <- function(x) {
#> ++ 2 | safe_mean <- function(x) {
#> 3 | if (length(x) == 0) return(NA)
#>
#> 5 | }
#> 6 |
#> [4] -- 7 | sd_safe <- function(x) {
#> ++ 7 | safe_sd <- function(x) {
#> 8 | if (length(x) <= 1) return(NA)
#>
#> 10 | }
#> 11 |
#> [5] -- 12 | print_vector <- function(v) {
#> ++ 12 | vector_print <- function(v) {
#> 13 | print(paste('Vector of length', length(v)))The listing, filtering, and matching steps can also be combined in
one step with seek(). seekr() is a convenience
wrapper around seek() that restricts the search to R, R
Markdown, and Quarto files (.R, .Rmd,
.qmd).
y <- seek(my_pattern, my_replacement, extension = "R")
z <- seekr(my_pattern, my_replacement)
identical(x, y)
#> [1] TRUE
identical(y, z)
#> [1] TRUEMatches can be filtered without reading the files again. Here, we
remove matches whose matched text contains "safe".
x <- filter_match(x, !grepl("safe", match))
print(x, context = c(3L, 1L))
#> <seekr::match[3]> 2 sources
#> Common Path: C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -- 1 | add_one <- function(x) {
#> ++ 1 | one_add <- function(x) {
#> 2 | return(x + 1)
#>
#> 6 | toupper(substr(txt, 1, 1))
#> 7 | }
#> 8 |
#> [2] -- 9 | say_hello <- function(name) {
#> ++ 9 | hello_say <- function(name) {
#> 10 | paste('Hello', name)
#>
#> script2.R [1]
#> 9 | sd(x, na.rm = TRUE)
#> 10 | }
#> 11 |
#> [3] -- 12 | print_vector <- function(v) {
#> ++ 12 | vector_print <- function(v) {
#> 13 | print(paste('Vector of length', length(v)))Replacements can also be updated after inspection. Here, we convert
the replacement to upper case when the word "hello" is in
the match text.
repl <- field(x, "replacement")
field(x, "replacement") = ifelse(
grepl("hello", field(x, "match")),
toupper(repl),
repl
)
print(x, context = 2L)
#> <seekr::match[3]> 2 sources
#> Common Path: C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -- 1 | add_one <- function(x) {
#> ++ 1 | one_add <- function(x) {
#> 2 | return(x + 1)
#> 3 | }
#>
#> 7 | }
#> 8 |
#> [2] -- 9 | say_hello <- function(name) {
#> ++ 9 | HELLO_SAY <- function(name) {
#> 10 | paste('Hello', name)
#> 11 | }
#>
#> script2.R [1]
#> 10 | }
#> 11 |
#> [3] -- 12 | print_vector <- function(v) {
#> ++ 12 | vector_print <- function(v) {
#> 13 | print(paste('Vector of length', length(v)))
#> 14 | }Now that they are ready, we can apply our selected replacements.
replace_files() starts from the current
seekr_match vector and replaces only the matches still
present in that vector, each with its corresponding replacement.
Before writing, replace_files() checks that every
selected match has a replacement and that the hash of each affected file
still matches the hash recorded when the seekr_match vector
was created. If a file has changed since the search, replacement stops
and the search should be run again on the current file contents.
replace_files(x)By default, replace_files() creates a backup in the
default backup_dir before modifying files. The latest
backup can be retrieved with last_backup().
bck <- last_backup()
bck
#> # A tibble: 2 × 9
#> id created_at operation description original backup original_exists backup_exists size
#> <int> <dttm> <chr> <chr> <chr> <chr> <lgl> <lgl> <fs:>
#> 1 1 2026-07-10 23:48:05 replace <NA> C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-ex… C:/Us… TRUE TRUE 172
#> 2 1 2026-07-10 23:48:05 replace <NA> C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-ex… C:/Us… TRUE TRUE 293Use restore_files() to restore the previous file
contents from the backup.
restore_files(from = bck$backup, to = bck$original)
#> ℹ Creating a backup of the current version of each existing destination file before restoring it.
#> ℹ This ensures you can revert to the state before restoration if needed.restore_files() also creates a backup, by default,
before restoring files.
list_backups()
#> # A tibble: 4 × 9
#> id created_at operation description original backup original_exists backup_exists size
#> <int> <dttm> <chr> <chr> <chr> <chr> <lgl> <lgl> <fs:>
#> 1 2 2026-07-10 23:48:05 restore <NA> C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-ex… C:/Us… TRUE TRUE 172
#> 2 2 2026-07-10 23:48:05 restore <NA> C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-ex… C:/Us… TRUE TRUE 293
#> 3 1 2026-07-10 23:48:05 replace <NA> C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-ex… C:/Us… TRUE TRUE 172
#> 4 1 2026-07-10 23:48:05 replace <NA> C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-ex… C:/Us… TRUE TRUE 293After restoring, the original matches are back.
x_restored <- seekr(my_pattern, my_replacement)
identical(z, x_restored)
#> [1] TRUE
print(x_restored, context = 2L)
#> <seekr::match[5]> 2 sources
#> Common Path: C:/Users/smarting/AppData/Local/Temp/Rtmpov8EM2/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -- 1 | add_one <- function(x) {
#> ++ 1 | one_add <- function(x) {
#> 2 | return(x + 1)
#> 3 | }
#>
#> 7 | }
#> 8 |
#> [2] -- 9 | say_hello <- function(name) {
#> ++ 9 | hello_say <- function(name) {
#> 10 | paste('Hello', name)
#> 11 | }
#>
#> script2.R [3]
#> 1 | # TODO: optimize this function
#> [3] -- 2 | mean_safe <- function(x) {
#> ++ 2 | safe_mean <- function(x) {
#> 3 | if (length(x) == 0) return(NA)
#> 4 | mean(x, na.rm = TRUE)
#> 5 | }
#> 6 |
#> [4] -- 7 | sd_safe <- function(x) {
#> ++ 7 | safe_sd <- function(x) {
#> 8 | if (length(x) <= 1) return(NA)
#> 9 | sd(x, na.rm = TRUE)
#> 10 | }
#> 11 |
#> [5] -- 12 | print_vector <- function(v) {
#> ++ 12 | vector_print <- function(v) {
#> 13 | print(paste('Vector of length', length(v)))
#> 14 | }The same seekr workflow can also easily be written as a
pipe.
x <-
list_files() |>
filter_files(extension = "R") |>
match_files(my_pattern, my_replacement) |>
filter_match(!grepl("safe", match)) |>
replace_files()seekr is designed to make search results actionable.
When your terminal supports OSC8 hyperlinks, printed matches include
clickable file locations, so you can inspect matches in the console and
jump directly to the corresponding file and line.
