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 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 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 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 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 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 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. Replacingconfig.ymlalso matchesconfigXyml; escape it, or use a delimiter that isn't in the text:sed -i 's|old/path|new/path|g'. - Commit first.
sed -ihas no undo. A clean git status before the replacement turns a mistake intogit checkout .. grep -lstops at the first match per file — that's what makes it fast, and it doesn't affect the replacement, sincesedstill 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
- Renaming hundreds of files: from
forloops to plain English - sed -i not working on Mac? BSD vs GNU flags, fixed
- From one-liner to checked-in script: when to graduate
Describe the replacement, read the command CliAI writes, then run it. Install it in one line.