When a hostname, API base URL, or feature flag must change in dozens of files, opening each one in an editor is the slow path. The durable CLI pattern is a pipeline: find selects the paths, xargs turns that list into command arguments, and sed rewrites the contents. This article owns that composition — dry-runs, null-safe names, and in-place edits with backups — not every flag on each tool.

You do not need a novel of options on day one. Build a playground, list the files you intend to touch, preview the sed rewrite on stdout, then only afterward write changes with .bak files. The sections below walk that path with enough context that each stage feels intentional, not magical.

Warm-up: build a small playground

Create a tiny project that looks like production noise: configs, source, a dependency folder you must skip, and at least one filename with a space.

rm -rf batch-demo
mkdir -p batch-demo/{src,config,docs,node_modules/pkg}
cat > batch-demo/src/app.js <<'EOF'
const API = "https://api.example.com/v1";
console.log("talking to api.example.com");
EOF
cat > batch-demo/config/app.conf <<'EOF'
host=api.example.com
env=prod
# DEPRECATED: remove in next release
legacy_mode=true
EOF
cat > batch-demo/config/staging.conf <<'EOF'
host=api.example.com
env=staging
EOF
echo 'notes about api.example.com' > 'batch-demo/docs/my notes.txt'
echo 'should stay untouched' > batch-demo/node_modules/pkg/index.js
echo 'api.example.com in vendor' >> batch-demo/node_modules/pkg/index.js

Everything interesting lives under batch-demo/. Recreate this tree if a later in-place edit goes sideways and you want a clean slate.

Select files with find

Batch edits fail first by touching the wrong files. Start by listing candidates — type, name, and prune rules — before any sed runs.

Regular files only, skipping node_modules:

find batch-demo -path '*/node_modules/*' -prune -o -type f -print

Typical list:

batch-demo/src/app.js
batch-demo/config/app.conf
batch-demo/config/staging.conf
batch-demo/docs/my notes.txt

Narrow to configs:

find batch-demo -path '*/node_modules/*' -prune -o -type f -name '*.conf' -print

For anything that will feed xargs, prefer NUL-delimited output so spaces in my notes.txt stay one path:

find batch-demo -path '*/node_modules/*' -prune -o -type f -print0 | xargs -0 -n 1 echo

Note: Deep find predicates live in the find guide. Here the rule is simple: if the path should never be edited, prune it before the print action, not after a careless sed -i.

Bridge the list with xargs

find … -print on stdout is not a file argument list. Piping paths into sed without xargs makes sed treat the names as stream text (or wait on stdin) — it will not open those files for in-place edit. xargs appends each path as an argv element to the command you name.

Dry-run the bridge with echo so you see the constructed command:

find batch-demo -path '*/node_modules/*' -prune -o -type f -name '*.conf' -print0 \
  | xargs -0 echo sed -i.bak 's/api.example.com/api.internal.net/g'
sed -i.bak s/api.example.com/api.internal.net/g batch-demo/config/app.conf batch-demo/config/staging.conf

Force one file per sed invocation with -n 1 or -I{} when you want clearer failures or a mid-command placeholder:

find batch-demo -path '*/node_modules/*' -prune -o -type f -name '*.conf' -print0 \
  | xargs -0 -I{} echo sed -i.bak 's/api.example.com/api.internal.net/g' {}

Note: Habit for trees you do not fully control: -print0 on find and -0 on xargs, always. Flag details and parallelism live in the xargs guide.

Preview and apply with sed

Never jump straight to -i on a whole tree. Preview the substitution on one file’s stdout first:

sed 's/api.example.com/api.internal.net/g' batch-demo/config/app.conf
host=api.internal.net
env=prod
# DEPRECATED: remove in next release
legacy_mode=true

When the preview looks right, run the same expression in place with a backup suffix, driven by the null-safe find|xargs pipeline:

find batch-demo -path '*/node_modules/*' -prune -o -type f \( -name '*.conf' -o -name '*.js' -o -name '*.txt' \) -print0 \
  | xargs -0 sed -i.bak 's/api.example.com/api.internal.net/g'

Confirm content and backups:

grep -R 'api.internal.net' batch-demo --exclude-dir=node_modules
ls batch-demo/config/*.bak batch-demo/src/*.bak

Vendor code under node_modules should still mention api.example.com. Restore a single file from its backup if needed:

mv batch-demo/config/app.conf.bak batch-demo/config/app.conf

Use a different delimiter when the pattern itself contains slashes:

sed 's|https://api.example.com/v1|https://api.internal.net/v1|g' batch-demo/src/app.js

Note: Substitution grammar and BRE vs ERE live in the sed guide. On this pipeline, the non-negotiable habit is -i.bak (or a git commit) before bulk writes — bare -i across a tree is how one bad pattern rewrites a repo with no undo.

Full pipelines you will reuse

Recreate the playground if you already mutated it (rm -rf batch-demo and rerun the warm-up). Then try these complete recipes.

1. Rename a token across configs and source (null-safe, with backups):

find batch-demo -path '*/node_modules/*' -prune -o -type f \( -name '*.conf' -o -name '*.js' \) -print0 \
  | xargs -0 sed -i.bak 's/api.example.com/api.internal.net/g'

2. Delete deprecated lines only in *.conf files:

Dry-run on one file:

sed '/DEPRECATED/d' batch-demo/config/app.conf

Apply across configs:

find batch-demo -type f -name '*.conf' -print0 \
  | xargs -0 sed -i.bak '/DEPRECATED/d'

3. Address-limited rewrite — change host= only in staging configs:

find batch-demo -type f -name 'staging.conf' -print0 \
  | xargs -0 sed -i.bak '/^host=/s/api.example.com/api.staging.internal.net/'

When find -exec is enough: a single action on a short list can skip xargs — find … -exec sed -i.bak 's/a/b/' {} +. Prefer the find|xargs form when you already dry-run with echo, need -I{}, or compose with other argv tools. Both are valid; pick the one the surrounding script reads more clearly.

Safety checklist

Run bulk edits in this order every time:

  1. Listfind … -print (or -print0 | xargs -0 -n 1 echo) and read the paths.
  2. Previewsed 's/…/…/' one-file on stdout; fix the expression before loops.
  3. Dry-run the argv… | xargs -0 echo sed -i.bak '…'.
  4. Apply with backups — drop the echo, keep -i.bak (or commit to git first).
  5. Verifygrep -R for the new and old tokens; spot-check that pruned dirs were spared.
  6. Rollbackmv file.bak file, or git checkout -- ., or an rsync copy you took beforehand.

Note: If the tree is already in git, a commit (or stash) before step 4 beats a pile of .bak files. Use backups anyway on machines where the files are not versioned.

Quick reference card

Keep these pipelines nearby until the composition is muscle memory:

GoalPipeline
List edit targetsfind dir -path '*/node_modules/*' -prune -o -type f -print
Null-safe echo dry-runfind … -print0 | xargs -0 echo sed -i.bak 's/a/b/'
Preview one filesed 's/a/b/g' file
In-place + backupfind … -print0 | xargs -0 sed -i.bak 's/a/b/g'
One file per sed… | xargs -0 -n 1 sed -i.bak 's/a/b/'
Placeholder form… | xargs -0 -I{} sed -i.bak 's/a/b/' {}
Delete matching lines… | xargs -0 sed -i.bak '/pat/d'
Slash-heavy patternsed 's|/old|/new|g' file
Verifygrep -R 'new-token' dir --exclude-dir=node_modules

Practice drills

Use batch-demo (recreate from the warm-up if needed). Try these without peeking. The point is to choose selection, bridging, and edit steps with intent.

  1. List every regular file under batch-demo except anything inside node_modules.
  2. Dry-run (with echo) a sed in-place that would replace prod with production in *.conf files only.
  3. Preview on stdout a global replace of api.example.comapi.internal.net in src/app.js.
  4. Apply that replace across *.conf, *.js, and *.txt, null-safe, with .bak backups, skipping node_modules.
  5. Remove lines containing DEPRECATED from all *.conf files (with backups).

When you are ready to compare, here are solid answers — not the only ones, but clear and portable:

find batch-demo -path '*/node_modules/*' -prune -o -type f -print

find batch-demo -type f -name '*.conf' -print0 \
  | xargs -0 echo sed -i.bak 's/prod/production/g'

sed 's/api.example.com/api.internal.net/g' batch-demo/src/app.js

find batch-demo -path '*/node_modules/*' -prune -o -type f \
  \( -name '*.conf' -o -name '*.js' -o -name '*.txt' \) -print0 \
  | xargs -0 sed -i.bak 's/api.example.com/api.internal.net/g'

find batch-demo -type f -name '*.conf' -print0 \
  | xargs -0 sed -i.bak '/DEPRECATED/d'

If you can work through those five comfortably, you already cover most real batch-edit work: select with find, bridge with xargs, rewrite with sed, and refuse to skip dry-runs and backups. Start with the file list, preview one substitute, then scale the same expression across the tree only when the preview matches the plan.