CLI AI

Find and replace across every file in a project

2026-05-03

The command is grep -rl 'old' . | xargs sed -i 's/old/new/g'. The important half is the first one: grep -rl lists the files that would change, so you can look before sed rewrites them in place.

1. Which files would change?

clai
$ clai list the files under proj that contain old_name→ grep -rl 'old_name' proj/proj/src/b.pyproj/src/a.py

-r recurses, -l prints filenames instead of matching lines. Two files — that's a believable number. If it says 4000, your pattern is too broad and you just saved yourself an afternoon.

2. Replace in exactly those files

clai
$ clai replace old_name with new_name in all files under proj that contain it→ grep -rl 'old_name' proj/ | xargs sed -i 's/old_name/new_name/g'$ cat proj/src/a.pynew_name = 1

Only the matching files are opened. Running sed -i over the whole tree instead would rewrite the mtime of every file in the repo, including the thousands that didn't change.

3. Filenames with spaces won't break it

clai
$ clai same replacement but handle filenames with spaces safely→ grep -rlZ 'old_name' proj/ | xargs -0 -r sed -i 's/old_name/new_name/g'

-Z makes grep separate names with a null byte and -0 makes xargs expect that. Without the pair, my file.py arrives as two arguments and sed edits neither. -r stops xargs from running sed with no arguments when nothing matched.

4. Skip the directories you never want to touch

clai
$ clai replace it but skip node_modules, .git and vendor→ grep -rlZ --exclude-dir={.git,node_modules,vendor} 'old_name' . | xargs -0 -r sed -i 's/old_name/new_name/g'

Without exclusions you will eventually rewrite a dependency's source, and the diff will be 40,000 lines long.

5. Only one file type

clai
$ clai replace it only in python files→ grep -rlZ --include='*.py' 'old_name' . | xargs -0 -r sed -i 's/old_name/new_name/g'

--include is the allowlist twin of --exclude. Narrower is safer.

6. Keep a backup — and stay portable

clai
$ clai do the replacement but keep a backup of each file, working on mac too→ grep -rlZ 'old_name' proj/ | xargs -0 -r sed -i.bak 's/old_name/new_name/g'

sed -i.bak writes a.py.bak next to each edited file. It's also the only spelling that works on both GNU and BSD sed — macOS requires the suffix, Linux accepts it.

Gotchas

  • Your pattern is a regex, not a string. ., *, / and $ are special. Replacing config.yml also matches configXyml; escape it, or use a delimiter that isn't in the text: sed -i 's|old/path|new/path|g'.
  • Commit first. sed -i has no undo. A clean git status before the replacement turns a mistake into git checkout ..
  • grep -l stops at the first match per file — that's what makes it fast, and it doesn't affect the replacement, since sed still processes every line.

Related questions

How do I preview the replacement instead of applying it? Drop -i and let it print: grep -rlZ 'old' . | xargs -0 sed 's/old/new/g' | less. Or grep -rn 'old' . to see every line with its number.

Why did nothing happen? Usually xargs got an empty list, or your pattern doesn't match what you think. Run the grep -rl half alone.

Can I do it with find instead? Yes: find . -type f -name '*.py' -exec sed -i 's/old/new/g' {} +. It rewrites every matched file whether or not it contained the pattern, which is why the grep -rl version is preferable.

See also

Describe the replacement, read the command CliAI writes, then run it. Install it in one line.