Introduction
seekr turns search-and-replace across files into an
explicit workflow.
This article gives a detailed walkthrough of that workflow. It starts from the basic steps—listing files, filtering files, and finding matches—and then shows how matches can be inspected, filtered, updated, replaced, and restored.
The examples use a temporary copy of the example files shipped with
seekr, so they are safe to modify.
List files
The first step is to list candidate files.
list_files() lists the files that exist and that will be
filtered before looking for matches. Its main arguments are
path, which defines where to look, all, which
controls whether hidden files and directories are included, and
recurse, which controls whether and how deeply
subdirectories are searched.
By default, list_files() searches recursively from the
current directory, and ignores hidden files and directories.
When searching inside a Git repository, you can also set
use_git = TRUE to restrict file discovery to files Git
considers relevant: tracked files and untracked files that are not
ignored by Git.
files <- list_files()
files
#> [1] "/tmp/RtmpxG7QUT/seekr-example/extdata/config.yaml" "/tmp/RtmpxG7QUT/seekr-example/extdata/data.json"
#> [3] "/tmp/RtmpxG7QUT/seekr-example/extdata/iris.csv" "/tmp/RtmpxG7QUT/seekr-example/extdata/mtcars.csv"
#> [5] "/tmp/RtmpxG7QUT/seekr-example/extdata/script1.R" "/tmp/RtmpxG7QUT/seekr-example/extdata/script2.R"
#> [7] "/tmp/RtmpxG7QUT/seekr-example/extdata/server1.log" "/tmp/RtmpxG7QUT/seekr-example/extdata/server2.log"list_files() is intentionally simple. Its role is to
discover candidate files, not to decide which files are relevant for a
particular search. That decision is handled later by
filter_files().
This separation is deliberate. File discovery can stay broad and predictable, while file filtering remains explicit and inspectable.
Filter files
Once files have been listed, filter_files() excludes the
files that should not be searched.
Built-in filters
filter_files() takes a vector of file paths, typically
returned by list_files(), and returns the subset of files
that should be searched.
It has three main built-in filters:
-
extension, to keep files with selected extensions, -
path_pattern, to keep files whose path matches a pattern, -
max_file_size, to keep files below a size limit.
In addition to these built-in filters, filter_files()
also uses a list of exclude_functions. These are
predicate-like functions that decide whether specific files should be
excluded. By default, they are used to exclude files that should usually
not be searched, such as non-text files or files handled by the default
exclusion rules.
For example, we can keep only R files.
filter_files(files, extension = "R")
#> [1] "/tmp/RtmpxG7QUT/seekr-example/extdata/script1.R" "/tmp/RtmpxG7QUT/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 /tmp/RtmpxG7QUT/seekr-example/extdata/config.yaml TRUE TRUE NA NA NA NA
#> 2 /tmp/RtmpxG7QUT/seekr-example/extdata/data.json TRUE TRUE NA NA NA NA
#> 3 /tmp/RtmpxG7QUT/seekr-example/extdata/iris.csv TRUE TRUE NA NA NA NA
#> 4 /tmp/RtmpxG7QUT/seekr-example/extdata/mtcars.csv TRUE TRUE NA NA NA NA
#> 5 /tmp/RtmpxG7QUT/seekr-example/extdata/script1.R FALSE FALSE FALSE FALSE FALSE FALSE
#> 6 /tmp/RtmpxG7QUT/seekr-example/extdata/script2.R FALSE FALSE FALSE FALSE FALSE FALSE
#> 7 /tmp/RtmpxG7QUT/seekr-example/extdata/server1.log TRUE TRUE NA NA NA NA
#> 8 /tmp/RtmpxG7QUT/seekr-example/extdata/server2.log TRUE TRUE NA NA NA NAHere we combine the different types of filters to exclude the files we are not interested in.
# Add a dummy png file to illustrate the exclusion of non-text files by default
files <- c(files, "server.png")
filtered <- filter_files(
files,
extension = c("r", "log", "yaml", "png"),
path_pattern = "script|server",
max_file_size = 1000L
)
filtered
#> [1] "/tmp/RtmpxG7QUT/seekr-example/extdata/script1.R" "/tmp/RtmpxG7QUT/seekr-example/extdata/script2.R"
#> attr(,"exclusions")
#> # A tibble: 9 × 9
#> path excluded exclude_by_extension exclude_by_path_patt…¹ exclude_by_file_size is_git_dir is_dependency_dir is_minified_file is_not_text_mime
#> <chr> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl>
#> 1 /tmp/Rtmpx… TRUE FALSE TRUE NA NA NA NA NA
#> 2 /tmp/Rtmpx… TRUE TRUE NA NA NA NA NA NA
#> 3 /tmp/Rtmpx… TRUE TRUE NA NA NA NA NA NA
#> 4 /tmp/Rtmpx… TRUE TRUE NA NA NA NA NA NA
#> 5 /tmp/Rtmpx… FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> 6 /tmp/Rtmpx… FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> 7 /tmp/Rtmpx… TRUE FALSE FALSE TRUE NA NA NA NA
#> 8 /tmp/Rtmpx… TRUE FALSE FALSE TRUE NA NA NA NA
#> 9 /tmp/Rtmpx… TRUE FALSE FALSE FALSE FALSE FALSE FALSE TRUE
#> # ℹ abbreviated name: ¹exclude_by_path_patternInspect exclusions
Filtering is inspectable. filter_files() records which
files were excluded and why.
In this example, files are first excluded by extension.
Then, among the remaining files, paths that do not match
path_pattern are excluded. Finally, the two log files are
excluded because they are above the size limit set with
max_file_size.
Each column in the exclusions table represents one filtering or
exclusion step. The last column shows that the dummy
"server.png" file was excluded by the default rules because
its extension is not associated with text files.
exclusions(filtered)
#> # A tibble: 9 × 9
#> path excluded exclude_by_extension exclude_by_path_patt…¹ exclude_by_file_size is_git_dir is_dependency_dir is_minified_file is_not_text_mime
#> <chr> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl>
#> 1 /tmp/Rtmpx… TRUE FALSE TRUE NA NA NA NA NA
#> 2 /tmp/Rtmpx… TRUE TRUE NA NA NA NA NA NA
#> 3 /tmp/Rtmpx… TRUE TRUE NA NA NA NA NA NA
#> 4 /tmp/Rtmpx… TRUE TRUE NA NA NA NA NA NA
#> 5 /tmp/Rtmpx… FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> 6 /tmp/Rtmpx… FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> 7 /tmp/Rtmpx… TRUE FALSE FALSE TRUE NA NA NA NA
#> 8 /tmp/Rtmpx… TRUE FALSE FALSE TRUE NA NA NA NA
#> 9 /tmp/Rtmpx… TRUE FALSE FALSE FALSE FALSE FALSE FALSE TRUE
#> # ℹ abbreviated name: ¹exclude_by_path_patternThis is useful because file filtering can otherwise be hard to audit.
Instead of silently excluding files, seekr lets you inspect
what was excluded at each step.
Custom exclude functions
In addition to the built-in filters, filter_files() uses
a list of exclude functions. These functions receive file paths and
return TRUE for files that should be excluded.
seekr provides a default set of exclude functions.
names(exclude_functions)
#> [1] "is_git_dir" "is_dependency_dir" "is_minified_file" "is_not_text_mime"You can add your own named exclude function by
modifying a copy of exclude_functions. Custom functions
will also appear on their dedicated column in the
exclusions() data frame.
my_exclude_functions <- c(
exclude_functions,
exclude_script2 = function(path) grepl("script2[.]R$", path)
)
filtered_custom <- filter_files(
files,
extension = "R",
exclude = my_exclude_functions
)
filtered_custom
#> [1] "/tmp/RtmpxG7QUT/seekr-example/extdata/script1.R"
#> attr(,"exclusions")
#> # A tibble: 9 × 8
#> path excluded exclude_by_extension is_git_dir is_dependency_dir is_minified_file is_not_text_mime exclude_script2
#> <chr> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl> <lgl>
#> 1 /tmp/RtmpxG7QUT/seekr-example/extdata/… TRUE TRUE NA NA NA NA NA
#> 2 /tmp/RtmpxG7QUT/seekr-example/extdata/… TRUE TRUE NA NA NA NA NA
#> 3 /tmp/RtmpxG7QUT/seekr-example/extdata/… TRUE TRUE NA NA NA NA NA
#> 4 /tmp/RtmpxG7QUT/seekr-example/extdata/… TRUE TRUE NA NA NA NA NA
#> 5 /tmp/RtmpxG7QUT/seekr-example/extdata/… FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> 6 /tmp/RtmpxG7QUT/seekr-example/extdata/… TRUE FALSE FALSE FALSE FALSE FALSE TRUE
#> 7 /tmp/RtmpxG7QUT/seekr-example/extdata/… TRUE TRUE NA NA NA NA NA
#> 8 /tmp/RtmpxG7QUT/seekr-example/extdata/… TRUE TRUE NA NA NA NA NA
#> 9 /tmp/RtmpxG7QUT/seekr-example/extdata/… TRUE TRUE NA NA NA NA NAThe order of exclude functions matters. They are evaluated in the
order in which they appear in exclude, so changing that
order can change which exclusion reason is recorded first. It can also
matter for performance: computationally expensive functions are usually
better placed near the end.
If no exclude functions are used, the exclusions table simply
contains the file paths and an excluded column set to
FALSE.
files |> filter_files(exclude = NULL) |> exclusions()
#> # A tibble: 9 × 2
#> path excluded
#> <chr> <lgl>
#> 1 /tmp/RtmpxG7QUT/seekr-example/extdata/config.yaml FALSE
#> 2 /tmp/RtmpxG7QUT/seekr-example/extdata/data.json FALSE
#> 3 /tmp/RtmpxG7QUT/seekr-example/extdata/iris.csv FALSE
#> 4 /tmp/RtmpxG7QUT/seekr-example/extdata/mtcars.csv FALSE
#> 5 /tmp/RtmpxG7QUT/seekr-example/extdata/script1.R FALSE
#> 6 /tmp/RtmpxG7QUT/seekr-example/extdata/script2.R FALSE
#> 7 /tmp/RtmpxG7QUT/seekr-example/extdata/server1.log FALSE
#> 8 /tmp/RtmpxG7QUT/seekr-example/extdata/server2.log FALSE
#> 9 /tmp/RtmpxG7QUT/seekr-example/extdata/server.png FALSEFind matches
Now that we have selected the files we want to search, we can look
for matches with match_files().
The two central arguments are pattern and
replacement. pattern defines what to look for.
replacement is optional: when provided, it prepares
replacement values for later, but it does not modify any file. Files are
only modified when replace_files() is called.
match_files() also has a few arguments that control how
files are read and how matches are recorded:
-
contextcontrols how many lines before and after each match are stored in theseekr_matchvector. -
encodingcontrols how files are decoded. By default, files are read as UTF-8. Setencoding = NULLif you wantseekrto try to detect the encoding withstringi::stri_enc_detect().
Regular expression patterns
Here we look for function names composed of two words separated by an
underscore followed by <- function.
matches <- match_files(filtered, "([a-z]+)_([a-z]+)(?= <- function)")
matches
#> <seekr::match[5]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -> 1 | add_one <- function(x) {
#> [2] -> 9 | say_hello <- function(name) {
#>
#> script2.R [3]
#> [3] -> 2 | mean_safe <- function(x) {
#> [4] -> 7 | sd_safe <- function(x) {
#> [5] -> 12 | print_vector <- function(v) {A plain character string is automatically treated as an ICU regular
expression via stringr::regex(), as in
stringr, except that multiline is set to
TRUE by default. Because multiline = TRUE,
anchors such as ^ and $ can match line
boundaries inside a file.
These two calls are therefore equivalent.
match_files(filtered, pattern)
match_files(
filtered,
stringr::regex(
"([a-z]+)_([a-z]+)(?= <- function)",
ignore_case = FALSE,
multiline = TRUE,
comments = FALSE,
dotall = FALSE
)
)For more control, pass a stringr pattern object
directly. For example, we can make the search case-insensitive.
match_files(filtered, stringr::regex("FUNCTION", ignore_case = TRUE))
#> <seekr::match[7]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/seekr-example/extdata
#>
#> script1.R [3]
#> [1] -> 1 | add_one <- function(x) {
#> [2] -> 5 | capitalize <- function(txt) {
#> [3] -> 9 | say_hello <- function(name) {
#>
#> script2.R [4]
#> [4] -> 1 | # TODO: optimize this function
#> [5] -> 2 | mean_safe <- function(x) {
#> [6] -> 7 | sd_safe <- function(x) {
#> [7] -> 12 | print_vector <- function(v) {For more details about regular expressions in stringr,
see the stringr
regular expressions documentation.
Literal text patterns
If you want to search for literal text instead of a regular
expression, use stringr::fixed().
match_files(filtered, stringr::fixed("<- function("))
#> <seekr::match[6]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/seekr-example/extdata
#>
#> script1.R [3]
#> [1] -> 1 | add_one <- function(x) {
#> [2] -> 5 | capitalize <- function(txt) {
#> [3] -> 9 | say_hello <- function(name) {
#>
#> script2.R [3]
#> [4] -> 2 | mean_safe <- function(x) {
#> [5] -> 7 | sd_safe <- function(x) {
#> [6] -> 12 | print_vector <- function(v) {This is useful when the text contains characters that would otherwise
have a special meaning in a regular expression. Note that
stringr::fixed() performs bytewise matching, so
stringr::coll() may be more appropriate for
locale-sensitive text.
For more details about the different pattern engines in
stringr, see the stringr
introduction.
Prepare replacements
Searching and replacing are separate steps in seekr.
You can prepare replacements when searching, but files are not
modified until you call replace_files(). This makes it
possible to search first, inspect the result, filter matches, update
replacements if needed, and only then write changes to disk.
seekr offers five ways to prepare replacements during
the matching step:
-
NULL, the default, to plan no replacement. - A plain string, used literally as replacement text.
- A string with backreferences of the form
\\1,\\2, and so on, replaced with the corresponding capture groups frompattern. - A function, called once per file with a character vector of all
matches found in that file, and expected to return a character vector of
the same length, such as
toupper(). - A function wrapped with
with_capture_groups_matrix(), called once per file with a character matrix where the first column is the full match and the remaining columns are the capture groups.
Literal replacements
The simplest replacement is a string.
match_files(
filtered,
pattern = stringr::fixed("safe"),
replacement = "checked"
)
#> <seekr::match[2]> 1 source
#> /tmp/RtmpxG7QUT/seekr-example/extdata/script2.R [2]
#> [1] -- 2 | mean_safe <- function(x) {
#> ++ 2 | mean_checked <- function(x) {
#> [2] -- 7 | sd_safe <- function(x) {
#> ++ 7 | sd_checked <- function(x) {Here, every match of "safe" gets the same planned
replacement: "checked".
Capture groups
Replacement strings can also refer to capture groups from a regular expression.
Here, we reverse the two parts of each function name.
match_files(filtered, "(\\w+)_(\\w+)(?= <- function)", "\\2_\\1")
#> <seekr::match[5]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -- 1 | add_one <- function(x) {
#> ++ 1 | one_add <- function(x) {
#> [2] -- 9 | say_hello <- function(name) {
#> ++ 9 | hello_say <- function(name) {
#>
#> script2.R [3]
#> [3] -- 2 | mean_safe <- function(x) {
#> ++ 2 | safe_mean <- function(x) {
#> [4] -- 7 | sd_safe <- function(x) {
#> ++ 7 | safe_sd <- function(x) {
#> [5] -- 12 | print_vector <- function(v) {
#> ++ 12 | vector_print <- function(v) {Function replacements
A replacement can also be a function. The function receives a character vector of matched texts and must return one replacement value per match. It is vectorized over the matches found in a file; it is not called separately for each individual match.
match_files(filtered, "([a-z]+)_([a-z]+)(?= <- function)", toupper)
#> <seekr::match[5]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -- 1 | add_one <- function(x) {
#> ++ 1 | ADD_ONE <- function(x) {
#> [2] -- 9 | say_hello <- function(name) {
#> ++ 9 | SAY_HELLO <- function(name) {
#>
#> script2.R [3]
#> [3] -- 2 | mean_safe <- function(x) {
#> ++ 2 | MEAN_SAFE <- function(x) {
#> [4] -- 7 | sd_safe <- function(x) {
#> ++ 7 | SD_SAFE <- function(x) {
#> [5] -- 12 | print_vector <- function(v) {
#> ++ 12 | PRINT_VECTOR <- function(v) {Capture group matrix replacements
For more complex replacements, wrap a function with
with_capture_groups_matrix().
The function receives the capture group matrix returned by the matching engine. The first column is the full match, and the following columns are the capture groups.
repl_with_matrix <- function(M) {
# full_match <- M[, 1L]
group1 <- M[, 2L]
group2 <- M[, 3L]
paste0(toupper(group2), "_", tolower(group1))
}
repl_with_matrix <- with_capture_groups_matrix(repl_with_matrix)
match_files(filtered, "([a-z]+)_([a-z]+)(?= <- function)", repl_with_matrix)
#> <seekr::match[5]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -- 1 | add_one <- function(x) {
#> ++ 1 | ONE_add <- function(x) {
#> [2] -- 9 | say_hello <- function(name) {
#> ++ 9 | HELLO_say <- function(name) {
#>
#> script2.R [3]
#> [3] -- 2 | mean_safe <- function(x) {
#> ++ 2 | SAFE_mean <- function(x) {
#> [4] -- 7 | sd_safe <- function(x) {
#> ++ 7 | SAFE_sd <- function(x) {
#> [5] -- 12 | print_vector <- function(v) {
#> ++ 12 | VECTOR_print <- function(v) {This is useful when the replacement logic depends on several captured parts of the match.
For workflows where text has already been read, or where you want to control reading and writing yourself, see Working with text.
Use seek() and seekr()
The previous sections used the lower-level steps explicitly. For
common workflows, seek() combines listing, filtering, and
matching in one call. seekr() is a shortcut around
seek() with R, R Markdown, and Quarto extensions selected
by default.
files <- list_files()
filtered <- filter_files(files, extension = "R")
x <- match_files(filtered, "(\\w+)_(\\w+)(?= <- function)", "\\2_\\1")
y <- seekr("(\\w+)_(\\w+)(?= <- function)", "\\2_\\1")
identical(x, y)
#> [1] TRUENote that:
- the vector of matches returned by
seekr()also contains theexclusions()attribute, - when no matches are found, the vector returned by
seek()/seekr()also contains anempty_stage()attribute that helps explain where the workflow became empty.
Inspect the seekr_match vector
The result of match_files(), seek(), and
seekr() is a seekr_match vector.
A seekr_match vector behaves like a vector of matches,
but each match also stores fields such as the file path, match location,
matched text, planned replacement, surrounding context, and
encoding.
str(x)
#> <seekr::match[5]> vctrs::rcrd
#> path <chr> "/tmp/RtmpxG7QUT/seekr-example/extdata/script1.R", "/tmp/RtmpxG7QUT/seekr-example/extdata/script1.R", "/tmp/RtmpxG7QUT/seekr-example…
#> start_line <int> 1, 9, 2, 7, 12
#> end_line <int> 1, 9, 2, 7, 12
#> start <int> 1, 107, 32, 119, 202
#> end <int> 7, 115, 40, 125, 213
#> 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, "\ncapitalize <- function(txt) {\n toupper(substr(txt, 1, 1))\n}\n", "# TODO: optimize this function", "mean_safe <- function(x…
#> 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)\n}\n\ncapitalize <- function(txt) {\n toupper(substr(txt, 1, 1))", " paste('Hello', name)\n}\n", " if (length(x)…
#> encoding <chr> "UTF-8", "UTF-8", "UTF-8", "UTF-8", "UTF-8"
#> hash <chr> "e4cc5c4031699a911e6d5029cce6d71c", "e4cc5c4031699a911e6d5029cce6d71c", "036951bf4066a0b69595b7a0d9d0eb96", "036951bf4066a0b69595b7a…You can inspect the available fields with fields().
fields(x)
#> [1] "path" "start_line" "end_line" "start" "end" "start_col" "end_col" "match" "replacement" "before"
#> [11] "line" "after" "encoding" "hash"Individual fields can be accessed with field().
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"
field(x, "path")
#> [1] "/tmp/RtmpxG7QUT/seekr-example/extdata/script1.R" "/tmp/RtmpxG7QUT/seekr-example/extdata/script1.R"
#> [3] "/tmp/RtmpxG7QUT/seekr-example/extdata/script2.R" "/tmp/RtmpxG7QUT/seekr-example/extdata/script2.R"
#> [5] "/tmp/RtmpxG7QUT/seekr-example/extdata/script2.R"Use summary() to get a compact overview of the matches
and planned replacements.
summary(x)
#> ── <seekr::match[5]> ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#> Common Path: /tmp/RtmpxG7QUT/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 matches with surrounding context
and preview replacements before modifying files.
print(x, context = c(2L, 1L))
#> <seekr::match[5]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/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)))In terminals that support OSC8 hyperlinks, printed file locations can also be clickable.
For more on why seekr represents search results this
way, see Design choices.
Filter matches
Because a seekr_match is a vector, it can be subset like
any other R vector.
x[!grepl("safe", field(x, "match"))]
#> <seekr::match[3]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -- 1 | add_one <- function(x) {
#> ++ 1 | one_add <- function(x) {
#> [2] -- 9 | say_hello <- function(name) {
#> ++ 9 | hello_say <- function(name) {
#>
#> script2.R [1]
#> [3] -- 12 | print_vector <- function(v) {
#> ++ 12 | vector_print <- function(v) {However, filter_match() is usually more convenient. It
evaluates expressions directly on the fields of the
seekr_match vector.
xf <- x |> filter_match(!grepl("safe", match))
xf
#> <seekr::match[3]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -- 1 | add_one <- function(x) {
#> ++ 1 | one_add <- function(x) {
#> [2] -- 9 | say_hello <- function(name) {
#> ++ 9 | hello_say <- function(name) {
#>
#> script2.R [1]
#> [3] -- 12 | print_vector <- function(v) {
#> ++ 12 | vector_print <- function(v) {The result is still a seekr_match vector, so it can be
printed, summarized, modified, or passed to
replace_files().
print(xf, context = c(2L, 1L))
#> <seekr::match[3]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/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 [1]
#> 10 | }
#> 11 |
#> [3] -- 12 | print_vector <- function(v) {
#> ++ 12 | vector_print <- function(v) {
#> 13 | print(paste('Vector of length', length(v)))Use tabular workflows
For many workflows, filter_match() and
field() are enough.
For more complex operations, it can be useful to convert a
seekr_match vector to a tibble, work with it using tabular
tools, and then convert it back to a seekr_match vector
before replacing files.
This is especially useful for grouped summaries, joins, group-aware
filtering, or replacement logic that is easier to express with
dplyr.
When converting back, as_match() validates that the
tibble still contains the fields required to reconstruct a valid
seekr_match vector. This makes it possible to use tabular
workflows while still returning to the main seekr
replacement workflow.
For examples, see the as_match() documentation and the
article Tabular workflows.
Update replacements after inspection
You do not need to decide every replacement when searching.
The replacement field can be set or updated after
matches have been inspected or filtered.
first_path <- field(xf, "path")[[1L]]
repl <- field(xf, "replacement")
field(xf, "replacement") <- ifelse(
field(xf, "path") == first_path,
toupper(repl),
tolower(repl)
)
print(xf, context = c(2L, 1L))
#> <seekr::match[3]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/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 [1]
#> 10 | }
#> 11 |
#> [3] -- 12 | print_vector <- function(v) {
#> ++ 12 | vector_print <- function(v) {
#> 13 | print(paste('Vector of length', length(v)))This makes it possible to search broadly, inspect the result, keep only the matches that matter, and then decide what each selected match should become.
Replace selected matches
Important: replace_files() writes
modified files in UTF-8. If you need to control how files are written,
use the workflow described in Working
with text.
When the selected matches and replacements look right, we can call
replace_files() which starts from the current
seekr_match vector.
If you found five matches, filtered the vector down to three matches, and updated their replacements, only those three remaining matches are replaced, 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
before writing that file, and the search should be run again on the
current file contents.
If you created several seekr_match vectors from the same
file state, combine them before replacing. After the first replacement,
the file hash changes, so a second call using matches from the old file
state will fail. For example, use replace_files(c(x, y))
instead of calling replace_files(x) and then
replace_files(y).
By default, backup = TRUE, so each file that will be
modified is backed up before it is written. A description
can be provided to make the backup easier to identify later. The backup
directory can also be changed with backup_dir or the
seekr.backup_dir option.
replaced <- replace_files(xf, description = "Inverse function names")
replaced
#> <seekr::match[3]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -- 1 | add_one <- function(x) {
#> ++ 1 | ONE_ADD <- function(x) {
#> [2] -- 9 | say_hello <- function(name) {
#> ++ 9 | HELLO_SAY <- function(name) {
#>
#> script2.R [1]
#> [3] -- 12 | print_vector <- function(v) {
#> ++ 12 | vector_print <- function(v) {After replacement, we can search again.
seekr("([a-z]+)_([a-z]+)(?= <- function)") |> print(context = c(0L, 3L))
#> <seekr::match[3]> 1 source
#> /tmp/RtmpxG7QUT/seekr-example/extdata/script2.R [3]
#> [1] -> 2 | mean_safe <- function(x) {
#> 3 | if (length(x) == 0) return(NA)
#> 4 | mean(x, na.rm = TRUE)
#> 5 | }
#>
#> [2] -> 7 | sd_safe <- function(x) {
#> 8 | if (length(x) <= 1) return(NA)
#> 9 | sd(x, na.rm = TRUE)
#> 10 | }
#>
#> [3] -> 12 | vector_print <- function(v) {
#> 13 | print(paste('Vector of length', length(v)))
#> 14 | }
#> 15 | Restore files
After modifying files, backups can be inspected with
list_backups(), and the most recent 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-11 20:15:22 replace Inverse function names /tmp/RtmpxG7QUT/seekr-example/extdata/script1… /tmp/… TRUE TRUE 161
#> 2 1 2026-07-11 20:15:22 replace Inverse function names /tmp/RtmpxG7QUT/seekr-example/extdata/script2… /tmp/… TRUE TRUE 279Use restore_files() to restore the previous file
contents.
restore_files(
from = bck$backup,
to = bck$original,
description = "restore after reversing function names by mistake"
)
#> ℹ 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.By default, restoring files also creates a backup before writing, so both operations remain available in the backup history. This makes it possible to undo a replacement, while still keeping a record of the files that were present just before the restore operation.
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-11 20:15:23 restore restore after reversing function names by mistake /tmp/RtmpxG7QUT/se… /tmp/… TRUE TRUE 161
#> 2 2 2026-07-11 20:15:23 restore restore after reversing function names by mistake /tmp/RtmpxG7QUT/se… /tmp/… TRUE TRUE 279
#> 3 1 2026-07-11 20:15:22 replace Inverse function names /tmp/RtmpxG7QUT/se… /tmp/… TRUE TRUE 161
#> 4 1 2026-07-11 20:15:22 replace Inverse function names /tmp/RtmpxG7QUT/se… /tmp/… TRUE TRUE 279After restoring, the original matches are back.
after_restore <- seekr("([a-z]+)_([a-z]+)(?= <- function)")
print(after_restore, context = c(2L, 1L))
#> <seekr::match[5]> 2 sources
#> Common Path: /tmp/RtmpxG7QUT/seekr-example/extdata
#>
#> script1.R [2]
#> [1] -> 1 | add_one <- function(x) {
#> 2 | return(x + 1)
#>
#> 7 | }
#> 8 |
#> [2] -> 9 | say_hello <- function(name) {
#> 10 | paste('Hello', name)
#>
#> script2.R [3]
#> 1 | # TODO: optimize this function
#> [3] -> 2 | mean_safe <- function(x) {
#> 3 | if (length(x) == 0) return(NA)
#>
#> 5 | }
#> 6 |
#> [4] -> 7 | sd_safe <- function(x) {
#> 8 | if (length(x) <= 1) return(NA)
#>
#> 10 | }
#> 11 |
#> [5] -> 12 | print_vector <- function(v) {
#> 13 | print(paste('Vector of length', length(v)))