How can I do a recursive find and replace from the command line?

92

36

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.

Nathan Long

Posted 2012-05-24T18:56:46.333

Reputation: 20 371

Related: Awk/Sed: How to do a recursive find/replace of a string?

– AlikElzin-kilaka – 2016-07-14T05:45:21.863

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. – Samy Bencherif – 2019-07-19T01:47:09.497

An alternative answer for the same questions can be found here http://stackoverflow.com/questions/9704020/recursive-search-and-replace-on-mac-and-linux

– dunxd – 2013-02-13T12:00:52.890

Answers

116

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:

  1. find . -type f -name '*.txt' finds, in the current directory (.) and below, all regular files (-type f) whose names end in .txt
  2. | passes the output of that command (a list of filenames) to the next command
  3. xargs gathers up those filenames and hands them one by one to sed
  4. sed -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)" (see man 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"

Nathan Long

Posted 2012-05-24T18:56:46.333

Reputation: 20 371

4What is the meaning of '' after the sed -i what is the '' role? – Jas – 2015-07-28T15:06:14.423

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 – 2015-12-03T15:30:26.703

what does -print0 do? – Jas – 2016-09-05T11:33:17.897

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 – 2016-11-22T19:03:55.673

1sed: can't read : No such file or directory – Karl Morrison – 2019-06-28T09:00:58.473

I am getting following error: sed: can't read : No such file or directory – alper – 2020-02-27T11:12:09.747

11Never ever pipe find output to xargs without the -print0 option. Your command will fail on files with spaces etc. in their name. – slhck – 2012-05-24T23:16:58.107

Why do you need sed -i '' -e 's/foo/bar/g'? Doesn't sed -i 's/foo/bar/g' do the same thing? – Daniel Andersson – 2012-05-25T07:19:32.573

20Also, just find -name '*.txt' -exec sed -i 's/foo/bar/g' {} + will do all this with GNU find. – Daniel Andersson – 2012-05-25T07:20:31.183

@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 – 2012-05-25T13:11:05.797

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 – 2012-05-25T16:28:25.143

@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 – 2012-06-01T19:34:55.473

7I 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 – 2014-01-30T11:00:02.640

9This works for me if I remove the space between the -i and the '' – Canadian Luke – 2014-06-02T19:49:47.157

30

find . -type f -name "*.txt" -exec sed -i'' -e 's/foo/bar/g' {} +

This removes the xargs dependency.

Ztyx

Posted 2012-05-24T18:56:46.333

Reputation: 412

1The accepted answers does a better job of explaining it. but +1 for using the correct syntax. – orodbhen – 2016-11-18T17:10:21.480

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 – 2013-01-16T09:59:11.297

10

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. )

David Winiecki

Posted 2012-05-24T18:56:46.333

Reputation: 243

1Big +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 – 2016-09-30T19:56:14.313

3

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)

Brian Agnew

Posted 2012-05-24T18:56:46.333

Reputation: 795

3

Try:

sed -i 's/foo/bar/g' $(find . -type f)

Tested on Ubuntu 12.04.

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"

nexayq

Posted 2012-05-24T18:56:46.333

Reputation: 206

Try 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 – 2015-11-08T06:43:45.827

1

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

Nathan Long

Posted 2012-05-24T18:56:46.333

Reputation: 20 371

0

# 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'

sMajeed

Posted 2012-05-24T18:56:46.333

Reputation: 1

0

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.

neo7

Posted 2012-05-24T18:56:46.333

Reputation: 356

0

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/'

John Morgan

Posted 2012-05-24T18:56:46.333

Reputation: 1

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 – 2015-11-08T06:36:35.867