Skip to content

Text Replacement

Use core.replace for find-and-replace operations on file contents.

Basic text replacement:

core.replace(
before = "old_text",
after = "new_text",
)
# Replace all occurrences
core.replace("internal.corp.com", "api.example.com")
# Equivalent explicit form
core.replace(
before = "internal.corp.com",
after = "api.example.com",
)
# Only replace in specific files
core.replace(
before = "internal.corp.com",
after = "api.example.com",
paths = glob(["**/*.md", "**/*.txt"]),
)

Use regex_groups for pattern matching:

core.replace(
before = "version = ${ver}",
after = "version = ${ver}-public",
regex_groups = {"ver": "[0-9]+\\.[0-9]+\\.[0-9]+"},
)

Groups are defined as ${name} in before/after and regex patterns in regex_groups:

# Capture and reuse groups
core.replace(
before = "TODO(${user}): ${message}",
after = "TODO: ${message}", # Remove user, keep message
regex_groups = {
"user": "[a-z]+",
"message": ".*",
},
)
# Version numbers
regex_groups = {"version": "[0-9]+\\.[0-9]+\\.[0-9]+"}
# Usernames
regex_groups = {"user": "[a-zA-Z_][a-zA-Z0-9_]*"}
# URLs
regex_groups = {"url": "https?://[^\\s]+"}
# File paths
regex_groups = {"path": "[a-zA-Z0-9_/.-]+"}

For patterns spanning multiple lines:

core.replace(
before = "// BEGIN INTERNAL\n${content}// END INTERNAL\n",
after = "",
regex_groups = {"content": "[\\s\\S]*?"}, # Non-greedy match
multiline = True,
)

Replace with empty string to remove:

# Remove single-line comments
core.replace(
before = "// INTERNAL: ${content}\n",
after = "",
regex_groups = {"content": ".*"},
)
# Remove block comments
core.replace(
before = "/* INTERNAL\n${content}*/\n",
after = "",
regex_groups = {"content": "[\\s\\S]*?"},
multiline = True,
)

For complex replacements with mapping:

core.filter_replace(
regex = "TODO\\(([a-z]+)\\)",
mapping = {
"alice": "team-frontend",
"bob": "team-backend",
},
)
# TODO(alice) → TODO(team-frontend)
# TODO(bob) → TODO(team-backend)
core.filter_replace(
regex = "@([a-z]+)\\.internal",
mapping = {
"alice": "alice@example.com",
"bob": "bob@example.com",
},
default = "support@example.com", # For unmatched users
)

Replacements are independent; order usually doesn’t matter unless patterns overlap:

transformations = [
# These are independent, order doesn't matter
core.replace("foo", "bar"),
core.replace("baz", "qux"),
]
transformations = [
# These overlap, order matters!
core.replace("foobar", "special"), # First: specific
core.replace("foo", "bar"), # Then: general
]
core.replace(
before = "https://internal.corp.com",
after = "https://api.example.com",
)
core.replace(
before = "@internal/",
after = "@public/",
paths = glob(["**/*.ts", "**/*.js"]),
)
core.replace(
before = "# INTERNAL: ${comment}\n",
after = "",
regex_groups = {"comment": ".*"},
)
core.replace(
before = "com.internal.${pkg}",
after = "com.public.${pkg}",
regex_groups = {"pkg": "[a-z.]+"},
paths = glob(["**/*.java"]),
)
core.replace(
before = '"version": "${ver}"',
after = '"version": "${ver}-public"',
regex_groups = {"ver": "[0-9.]+"},
paths = glob(["package.json"]),
)

Special regex characters need escaping:

# Match literal dots
core.replace(
before = "example\\.com",
after = "example.org",
)
# Match literal braces
core.replace(
before = "\\{\\{template\\}\\}",
after = "{{new_template}}",
)

Test replacements locally:

Terminal window
# Use folder destination to preview
java -jar copybara.jar migrate copy.bara.sky workflow \
--folder-destination /tmp/output
# Check specific files
diff original/file.txt /tmp/output/file.txt