Multi-File Editing

Multi-file editing is the practice of applying changes across a set of files as a single operation, rather than opening and modifying each file individually. It matters when a change pattern repeats across dozens, hundreds, or thousands of files—whether you’re renaming a function across a codebase, updating a configuration directive in every service manifest, or batch-converting a data format. The orientation can be mechanical (automating repetitive keystrokes) or semantic (ensuring every file that declares a certain feature actually implements it correctly). The tooling varies by environment, but the underlying patterns—locate, transform, verify—are consistent.

Locate the target set

Before any edit can be applied, you need to know which files are in scope. The most common pattern is pattern-based selection: find every file that contains or lacks a particular string, then operate on that list.

If you’re working within a Git repository, git grep restricts search to tracked files and respects .gitignore:

git grep -l "deprecatable" .

The -l flag suppresses matching lines and prints only filenames. For cross-repository or filesystem-wide searches, find combined with grep is the portable building block:

find . -type f -name "*.conf" | xargs grep -l "listen_address"

find’s -type f filters regular files; pipe the results into xargs grep -l to get a list of files whose content matches the pattern. On systems with GNU grep, the -r (recursive) flag is available, but find gives you finer control over which filenames enter the pipeline—useful when you want to exclude certain directories or limit by extension.

Ripgrep (rg) is a modern alternative that many engineers replace grep with entirely:

rg -l "schema_version" --glob '!**/vendor/*'

The --glob flag lets you exclude paths using the same syntax as .gitignore. rg is faster than grep for most workloads, but it isn’t always installed on minimal server images; keep the find | xargs pattern as a fallback.

Once you have a list of filenames, you can save it for later:

git grep -l "deprecatable" . > /tmp/targets.txt

Apply a uniform text replacement

With a file list in hand, the next goal is usually the same change everywhere—say, replacing a deprecated flag with its successor, or updating a URL across every configuration file.

Using sed in-place

sed -i performs in-place substitution. On GNU/Linux, the syntax is straightforward:

sed -i 's/old_flag/new_flag/g' $(cat /tmp/targets.txt)

The g flag after the trailing slash replaces all occurrences on each line, not just the first. If the file list contains paths with spaces, the command above breaks. A safer form uses find with -exec:

find . -type f -name "*.conf" -exec sed -i 's/old_flag/new_flag/g' {} +

The + at the end of -exec causes find to build argument batches, invoking sed once per group of files rather than once per file. This is both faster and more robust with unusual filenames.

Handling the macOS difference

On macOS, sed -i requires a backup extension argument, even if you don’t want one:

sed -i '' 's/old_flag/new_flag/g' filename

The empty string '' satisfies the requirement, but it’s a source of friction when scripts target both platforms. A portable workaround is to write to a temporary file and move it:

sed 's/old_flag/new_flag/g' filename > filename.tmp && mv filename.tmp filename

Using Vim in batch mode

If your workflow already involves Vim or Neovim, you can apply changes across the argument list without leaving the editor. Load the target files as arguments:

vim -p $(cat /tmp/targets.txt)

Inside Vim, :argdo runs a command across all files in the argument list:

:argdo %s/old_flag/new_flag/g | update

The update writes the file only if it was modified, avoiding unnecessary writes. For per-buffer rather than per-argument-list iteration, :bufdo does the same but cycles through open buffers. In Neovim, the same commands work, and you can additionally use :handlers or Lua scripting for more complex batch logic, but the classic :argdo pattern remains the most portable.

Avoiding common pitfalls

  • sed -i on macOS silently fails if you omit the backup extension argument. The command runs without error, but no changes are written.
  • When using find -exec ... {} +, if a filename contains a newline, find may split it across invocations. Newlines in filenames are unusual but possible; find -print0 combined with xargs -0 handles them correctly.
  • sed basic regex and extended regex (sed -E or sed) differ in metacharacter treatment. If your pattern contains ( or ), you may need the -E flag or escape them.

Apply distinct edits to multiple files in bulk

Sometimes the change isn’t uniform. You may need to set different values depending on the file’s role—e.g., increasing the replica count in a deployment manifest for the frontend service but not for the backend. In these cases, you’re orchestrating a scripted pass over each file.

Using xargs with a shell snippet

cat /tmp/targets.txt | xargs -I{} sh -c 'sed -i "s/replicas: 2/replicas: 3/" {}'

The -I{} flag lets you substitute the full filename into the command string. This works for simple one-liners, but becomes unwieldy if the replacement logic itself is conditional.

Using a short script

A more readable approach is a three-line shell script piped into bash:

while IFS= read -r filepath; do
  if [[ "$filepath" == *frontend* ]]; then
    sed -i 's/replicas: 2/replicas: 3/' "$filepath"
  else
    sed -i 's/replicas: 2/replicas: 5/' "$filepath"
  fi
done < /tmp/targets.txt

Read line by line from the file list, apply different edits based on conditions, and move to the next. The IFS= read -r pattern prevents backslash interpretation and preserves leading/trailing whitespace in paths.

Using Python for complex logic

When the condition involves parsing file contents, computing new values, or writing JSON/YAML, a one-off Python one-liner often beats shell gymnastics:

python3 -c "
import pathlib, sys, json
for f in pathlib.Path('.').glob('*.yaml'):
    data = json.loads(open(f).read())
    if 'frontend' in str(f):
        data['replicas'] = 3
    else:
        data['replicas'] = 5
    open(f, 'w').write(json.dumps(data))
"

This reads each file, applies branch logic, and writes back. It handles YAML that happens to be valid JSON (a common pattern in lightweight configs), and if the file format is strictly YAML, pyyaml can be imported instead of json. The trade-off is that Python may not be available on minimal containers or embedded devices; in those cases, stick to sed or shell loops.

Review and confirm what changed

Any bulk edit carries risk. The habit of always checking before proceeding—and after—distinguishes careful engineers from those who learn the hard way.

Git-aware workflow

If the files are tracked in Git, git diff after the edit shows exactly what changed:

git diff

To see which files were modified (without the diff content):

git diff --name-only

If you used sed -i and didn’t commit intermediate steps, git diff still works because Git tracks file content, not the method of modification. The working tree will reflect the new content, and git diff will show the diff against the last commit.

Non-Git environments

When files aren’t under version control, or you’re editing runtime configs where a Git repo isn’t appropriate, external verification is needed. For JSON files, jq can validate structure and diff the before/after:

jq . before.json > /dev/null && echo "valid JSON"

If you kept pre-edit copies (e.g., by using sed without -i and redirecting to a temp file), diff between the original and modified versions is straightforward:

diff -u original.json modified.json

Automated linting or schema validation

In many organizations, multi-file edits are followed by a CI step that runs a linter or JSON schema validator across all changed files. If your tooling supports it, run that step manually before committing:

prettier --write .

kubeval --kubernetes-version v1.28 .

These commands may report errors introduced by the edit—missing commas, invalid enum values, or schema violations. Resolve those before proceeding.

Undo or roll back

If the edit introduced errors, or the scope was wider than intended, you need a reliable undo path.

Git-based recovery

If the files are Git-tracked and you haven’t committed yet, git reset --hard HEAD discards all working-tree changes:

git reset --hard HEAD

If you’ve already committed the bad edit, git revert creates a new commit that inverts the changes:

git revert HEAD

For sed -i edits where no backup was kept and the files aren’t tracked, recovery is harder. Some versions of sed create backup files with a .bak suffix when an extension is provided (sed -i .bak), but this isn’t guaranteed across platforms. If you have a backup system or artifact repository (Artifactory, S3 bucket with versioning, etc.), check there first.

Versioned configuration

In environments where configs are deployed from a source-of-truth repo (Helm charts, Kustomize overlays, Terraform modules), the “undo” is simply reverting the manifest in version control and re-applying. The edit never touches runtime files directly; it modifies the source, and the deployment tool propagates the change. This pattern is worth advocating for when multi-file editing is a regular requirement.

Sharp edges and production lessons

Filenames with spaces

xargs by default splits on whitespace. A file named my config.yaml becomes two arguments: my and config.yaml. Always use find -print0 | xargs -0 when spaces are possible:

find . -type f -name "*.conf" -print0 | xargs -0 sed -i 's/old/new/g'

macOS vs GNU sed differences

Beyond the backup extension quirk, some sed flags differ. The \n newline metacharacter, character classes like [:space:], and the handling of quantifiers can vary. When in doubt, test on the target platform before rolling a script out fleet-wide.

Regex greediness

sed and grep use greedy matching by default. If your pattern can match more than intended, you may get fewer substitutions than expected, or substitutions in files you didn’t mean to touch. Anchor patterns with ^ or $ when precision is required, or use more specific context in the pattern itself.

Partial-line matches

sed 's/foo/bar/g' replaces foo wherever it appears on a line, even if foo is part of a larger word like foobar. If you only want to replace whole-word instances, you need word-boundary assertions, which sed doesn’t natively support in a portable way. Some engineers fall back to perl -pi -e 's/\bfoo\b/bar/g' where Perl is available, but that adds a dependency.

Silent no-ops

A common silent failure: find ... -exec sed -i ... {} + runs without error, but no files were matched because the pattern didn’t exist in any file, or the paths were incorrect. The command succeeds because sed is happy to process zero input lines. Always verify the file count before and after, or add an echo statement to confirm files were visited:

find . -type f -name "*.conf" -exec sed -i 's/old/new/g' {} + && echo "sed completed"

without a follow-up check, you might think the edit succeeded when nothing was actually changed.

Overlapping edit scopes

If you run the same multi-file edit twice—say, you forgot the first run completed and ran the command again—the second invocation may produce unexpected results. With sed -i, the second pass operates on already-modified content. If the pattern matches the replacement text from the first pass, you get cascaded substitutions. Always confirm the state of the files before re-running.

Concurrent edits

If multiple engineers or automation processes are editing the same file set simultaneously, race conditions can produce lost updates or merge conflicts. Distributed version control (Git) mitigates this through merge workflows, but in non-Git environments, file-level locking or deployment pipelines that serialize changes are necessary.

Summary

Multi-file editing reduces repetitive labor and increases consistency, but each shortcut—sed -i on a remote fleet, xargs without -0, a Python script run as root—carries failure modes that only appear when something goes wrong. The pattern is always the same: locate the file set with precision, apply the transformation with a tool you’ve tested, verify the result, and have a known recovery path. Stick to portable building blocks (find, grep, sed, git) when possible, and only introduce language-specific or tool-specific helpers when the problem genuinely outgrows them. The goal isn’t to make the edit happen; it’s to make it happen correctly, and to know how to undo it if it doesn’t.

This page was rewritten on 14 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.