Using a shell like bash or zshell, how can I do a recursive 'find and replace'? In other words, I want to replace every occurrence of 'foo' with 'bar' in all files in this directory and its subdirectories.
-
1An alternative answer for the same questions can be found here http://stackoverflow.com/questions/9704020/recursive-search-and-replace-on-mac-and-linux – dunxd Feb 13 '13 at 12:00
-
Related: [Awk/Sed: How to do a recursive find/replace of a string?](http://stackoverflow.com/q/1583219/435605) – AlikElzin-kilaka Jul 14 '16 at 05:45
-
It might be a good idea to try this in vim. That way you can use the confirmation feature to make sure you don't swap something you don't intend to. I am not sure if it can be done directory wide. – Samie Bencherif Jul 19 '19 at 01:47
10 Answers
This command will do it (tested on both Mac OS X Lion and Kubuntu Linux).
# Recursively find and replace in files
find . -type f -name "*.txt" -print0 | xargs -0 sed -i '' -e 's/foo/bar/g'
Here's how it works:
find . -type f -name '*.txt'finds, in the current directory (.) and below, all regular files (-type f) whose names end in.txt|passes the output of that command (a list of filenames) to the next commandxargsgathers up those filenames and hands them one by one tosedsed -i '' -e 's/foo/bar/g'means "edit the file in place, without a backup, and make the following substitution (s/foo/bar) multiple times per line (/g)" (seeman sed)
Note that the 'without a backup' part in line 4 is OK for me, because the files I'm changing are under version control anyway, so I can easily undo if there was a mistake.
To avoid having to remember this, I use an interactive bash script, as follows:
#!/bin/bash
# find_and_replace.sh
echo "Find and replace in current directory!"
echo "File pattern to look for? (eg '*.txt')"
read filepattern
echo "Existing string?"
read existing
echo "Replacement string?"
read replacement
echo "Replacing all occurences of $existing with $replacement in files matching $filepattern"
find . -type f -name $filepattern -print0 | xargs -0 sed -i '' -e "s/$existing/$replacement/g"
- 26,015
- 36
- 102
- 139
-
14Never ever pipe find output to xargs without the `-print0` option. Your command will fail on files with spaces etc. in their name. – slhck May 24 '12 at 23:16
-
1Why do you need `sed -i '' -e 's/foo/bar/g'`? Doesn't `sed -i 's/foo/bar/g'` do the same thing? – Daniel Andersson May 25 '12 at 07:19
-
32Also, just `find -name '*.txt' -exec sed -i 's/foo/bar/g' {} +` will do all this with GNU find. – Daniel Andersson May 25 '12 at 07:20
-
@DanielAndersson - I seem to be getting different results on my work Mac and home Ubuntu machines, so I need to do some more tinkering before I update this answer. One note, though: the `find... -exec` method is interesting, but `man find` says to use '-execdir' instead for security reasons. – Nathan Long May 25 '12 at 13:11
-
Yeah, I've read that remark on `-execdir` in the manual, but I have never been or heard of anyone who has been bitten, so until then I stay with `-exec` :-) . That you get different results on Mac OS X and Ubuntu is because Mac OS X does not come with `GNU find` as standard, and `-exec +` and the omission of the path are `GNU find` specific extensions. Perhaps the same could explain some `sed` discrepancies between the systems. – Daniel Andersson May 25 '12 at 16:28
-
@DanielAndersson - It appears that you're correct; my Linux box can do the `find... -exec` version, but my Mac can't. `sed - 's/foo/bar/g'` does not work; `-i` is to provide a backup file extension, and giving it an empty string means 'don't back up.' The command in my answer works on both platforms. – Nathan Long Jun 01 '12 at 19:34
-
9I get `sed: can't read : No such file or directory` when I run `find . -name '*.md' -print0 | xargs -0 sed -i '' -e 's/ä/ä/g'`, but `find . -name '*.md' -print0` gives a list of many files. – Martin Thoma Jan 30 '14 at 11:00
-
14This works for me if I remove the space between the `-i` and the `''` – Canadian Luke Jun 02 '14 at 19:49
-
6
-
1@Jas: `-i` with `sed` means perform an in-place edit and `''` (empty string) means don't create backups. If you did `sed -i '.bak'` the original files would be preserved with a `.bak` extension. – Michael Thompson Dec 03 '15 at 15:30
-
-
I don't like this solution, as for me, it 'touches' every .txt file and bumps the file modification time, even if no changes are made... – daryl Nov 22 '16 at 19:03
-
2
-
I am getting following error: `sed: can't read : No such file or directory` – alper Feb 27 '20 at 11:12
-
1See https://superuser.com/questions/677078/sed-cant-read-no-such-file-or-directory for a solution to the `no such file or directory` error – Luc H Sep 24 '21 at 06:57
-
@KarlMorrison I got the same error. I replaced "print0" with "print" and it worked. – Erel Segal-Halevi Apr 20 '22 at 19:43
find . -type f -name "*.txt" -exec sed -i'' -e 's/foo/bar/g' {} +
This removes the xargs dependency.
- 732
- 5
- 9
-
4This does not work with GNU `sed`, so will fail on most systems. GNU `sed` requires you to put no space between `-i` and `''`. – slhck Jan 16 '13 at 09:59
-
2The accepted answers does a better job of explaining it. but +1 for using the correct syntax. – orodbhen Nov 18 '16 at 17:10
-
1
If you're using Git then you can do this:
git grep -lz foo | xargs -0 sed -i '' -e 's/foo/bar/g'
-l lists only filenames. -z prints a null byte after each result.
I ended up doing this because some files in a project did not have a newline at the end of the file, and sed added a newline even when it made no other changes. (No comment on whether or not files should have a newline at the end. )
- 363
- 2
- 8
-
2Big +1 for this solution. The rest of the `find ... -print0 | xargs -0 sed ...` solutions not only take a lot longer but also add newlines to files that don't have one already, which is a nuisance when working inside a git repo. `git grep` is lighting fast by comparison. – Josh Kupershmidt Sep 30 '16 at 19:56
-
Not sure what the single quote pair does in the sed command. I think it was causing a "no such file" error. Worked great without them. – David Lotts Dec 28 '20 at 23:04
-
May be a difference between gnu sed and other sed implementations. – David Winiecki Dec 30 '20 at 04:23
-
This brought down the time taken for my script from 7 minutes to 40 seconds! Thank you for this solution – schizovivek Jul 07 '23 at 22:15
Try:
sed -i 's/foo/bar/g' $(find . -type f)
Tested on Ubuntu 12.04.
EDIT:
This command will NOT work if subdirectory names and/or filenames contain spaces, but if you do have them don't use this command as it won't work.
It is generally a bad practice to use spaces in directory names and filenames.
http://linuxcommand.org/lc3_lts0020.php
Look at "Important facts about file names"
- 264
- 3
- 4
-
1Try it when you have file(s) with space(s) in their names. (There's a rule of thumb that says, "If something seems too good to be true, it probably is." If you have "discovered" a solution that's more compact than anything anybody else has posted in 3½ years, you should ask yourself why that might be.) – Scott - Слава Україні Nov 08 '15 at 06:43
-
1Files with spaces are fairly uncommon in linux. Fair to list the caveat but IMO this is the best answer. – Jordan Jan 14 '22 at 19:31
Here's my zsh/perl function I use for this:
change () {
from=$1
shift
to=$1
shift
for file in $*
do
perl -i.bak -p -e "s{$from}{$to}g;" $file
echo "Changing $from to $to in $file"
done
}
And I'd execute it using
$ change foo bar **/*.java
(for example)
- 1,055
- 6
- 7
Use This Shell Script
I now use this shell script, which combines things I learned from the other answers and from searching the web. I placed it in a file called change in a folder on my $PATH and did chmod +x change.
#!/bin/bash
function err_echo {
>&2 echo "$1"
}
function usage {
err_echo "usage:"
err_echo ' change old new foo.txt'
err_echo ' change old new foo.txt *.html'
err_echo ' change old new **\*.txt'
exit 1
}
[ $# -eq 0 ] && err_echo "No args given" && usage
old_val=$1
shift
new_val=$1
shift
files=$* # the rest of the arguments
[ -z "$old_val" ] && err_echo "No old value given" && usage
[ -z "$new_val" ] && err_echo "No new value given" && usage
[ -z "$files" ] && err_echo "No filenames given" && usage
for file in $files; do
sed -i '' -e "s/$old_val/$new_val/g" $file
done
- 26,015
- 36
- 102
- 139
# Recursively find and replace in files
find . -type f -name "*.txt" -print0 | xargs -0 sed -i '' -e 's/foo/bar/g'
The above worked like a charm, but with linked directories, I've to add -L flag to it. The final version looks like:
# Recursively find and replace in files
find -L . -type f -name "*.txt" -print0 | xargs -0 sed -i '' -e 's/foo/bar/g'
- 101
- 1
Using zsh globbing and sed
(Tested with sed on MacOS - may vary a bit on Linux)
sed -i '' -e 's/foo/bar/g' somefolder/**/*.txt
- 26,015
- 36
- 102
- 139
My use case was I wanted to replace
foo:/Drive_Letter with foo:/bar/baz/xyz
In my case I was able to do it with the following code.
I was in the same directory location where there were bulk of files.
find . -name "*.library" -print0 | xargs -0 sed -i '' -e 's/foo:\/Drive_Letter:/foo:\/bar\/baz\/xyz/g'
hope that helped.
- 717
- 5
- 6
The following command worked fine on Ubuntu and CentOS; however, under OS X I kept getting errors:
find . -name Root -exec sed -i 's/1.2.3.4\/home/foo.com\/mnt/' {} \;
sed: 1: "./Root": invalid command code .
When I tried passing the params via xargs it worked fine with no errors:
find . -name Root -print0 | xargs -0 sed -i '' -e 's/1.2.3.4\/home/foo.com\/mnt/'
- 9,037
- 14
- 59
- 71
-
The fact that you changed `-i` to `-i ''` is probably more relevant than the fact that you changed `-exec` to `-print0 | xargs -0`. BTW, you probably don't need the `-e`. – Scott - Слава Україні Nov 08 '15 at 06:36