2

I need to recursively find and replace a string in my .cpp and .hpp files.

Looking at an answer to this question I've found the following command:

find /home/www -type f -print0 | xargs -0 sed -i 's/subdomainA.example.com/subdomainB.example.com/g'

Changing it to include my file type did not work - did not changed any single word:

find /myprojects -type f -name *.cpp -print0 | xargs -0 sed -i 's/previousword/newword/g'

Help appreciated.

Community
  • 1
  • 1
Mendes
  • 17,489
  • 35
  • 150
  • 263
  • 1
    You must _quote_ `*.cpp` - e.g., as `'*.cpp'` - to prevent premature expansion of the glob by the _shell_. – mklement0 Oct 31 '15 at 18:39
  • Hummm. still not working... `find /myprojects -type f -name '*.cpp' -print0 | xargs -0 sed -i 's/previousword/newword/g'`. – Mendes Oct 31 '15 at 18:42
  • Isolate the problem by testing whether the `find` command itself works correctly, and test the `sed` command on a single file. Sometimes you get away with not quoting the glob - if the current dir. happens to contain no matches and `shopt -s nullglob` is not in effect - so it may not be the (only) source of the problem, but generally you should _always_ quote. – mklement0 Oct 31 '15 at 18:48
  • Also note that if you're on OS X / a BSD system, you must use `sed -i ''` instead of just `sed i` – mklement0 Oct 31 '15 at 18:51
  • I´m on Ubuntu... I´ve tested. `sed` expects the filename to be at the end of the command. I tried also `find /myprojects -type f -name '*.cpp' -print0 | xargs sed -i 's/previousword/newword/g' -0` with no success.. `sed -i s/word1/word2/g' file.cpp` works, `sed file.cpp -i s/word1/word2/g'` does not.... How to move the filename to the end of the `sed` command ? – Mendes Oct 31 '15 at 19:10
  • 1
    You don't need to move the filenames to the end, because `xargs` places them there by default. If you quote `*.cpp`, there's nothing _conceptually_ wrong with your command, though @chepner's `-exec` solution below is preferable, albeit with `+` rather than `\;`. – mklement0 Oct 31 '15 at 20:58
  • Sorry for the delay in answering... I came back to this today and it´s working fine... – Mendes Nov 10 '15 at 14:52

2 Answers2

4

Don't bother with xargs; use the -exec primary. (Split across two lines for readability.)

find /home/www -type f -name '*.cpp' \
  -exec sed -i 's/previousword/newword/g' '{}' \;
chepner
  • 497,756
  • 71
  • 530
  • 681
  • Humm... I don´t think this is working, even correcting it: `find /home/www -type -f -name ' *.cpp' \ -exec sed -i 's/previousword/newword/g' '{}' +`... – Mendes Oct 31 '15 at 19:27
  • @Mendez There's a space in your name pattern: `' *.cpp'`. It should be `'*.cpp'`. – melpomene Oct 31 '15 at 19:31
  • Type error in so.... Your post has errors (souble `previousword` and missing + at the end)... But still does not work in Ubuntu... – Mendes Oct 31 '15 at 19:35
  • @Mendez: There's another typo in your command: `-f` instead of `f`. While using `+` is _preferable_, because it will invoke `sed` only _once_ (typically), `\;` should work too. "Does not work" is not sufficient to diagnose the problem. – mklement0 Oct 31 '15 at 20:55
  • Ok about the typo, but is´t only a typo. "Does not work" means the given command `find /home/www -type f -name '*.cpp' -exec sed -i 's/word1/word2/g' '{}' +` searches the files but does not change its words as expected - the target .cpp and .hpp remains the same... `sed` as it is works in a single file if we put the filename at the end: `sed -i 's/word1/word2/g' file.cpp`. `find` works well showing all .cpp or .hpp files, but both together searches the files but does not change the texts as expected. – Mendes Oct 31 '15 at 21:18
  • Typo? Your code: `'s/previousword/s/previousword/newword/g'`, I would expect `'s/previousword/newword/g'`. Including a warning about words with a `/` – Walter A Oct 31 '15 at 22:21
  • I think I copied the `sed` command after starting to type it by hand. Fixing. – chepner Nov 01 '15 at 02:36
2

chepner's helpful answer proposes the simpler and more efficient use of find's -exec action instead of piping to xargs.

Unless special xargs features are needed, this change is always worth making, and maps to xargs features as follows:

  • find ... -exec ... {} \; is equivalent to find ... -print0 | xargs -0 -n 1 ...
  • find ... -exec ... {} + is equivalent to find ... -print0 | xargs -0 ...

In other words:

  • the \; terminator invokes the target command once for each matching file/folder.

  • the + terminator invokes the target command once overall, supplying all matching file/folder paths as a single list of arguments.

    • Multiple calls happen only if the resulting command line becomes too long, which is rare, especially on Linux, where getconf ARG_MAX, the max. command-line length, is large.

Troubleshooting the OP's command:

Since the OP's xargs command passes all matching file paths at once - and per xargs defaults at the end of the command line, the resulting command will effectively look something like this:

  sed -i 's/previousword/newword/g' /myprojects/file1.cpp /myprojects/file2.cpp ...

This can easily be verified by prepending echo to sed - though (conceptual) quoting of arguments that need it (paths with, e.g., embedded spaces) will not show (note the echo):

find /myprojects -type f -name '*.cpp' -print0 | 
  xargs -0 echo sed -i 's/previousword/newword/g'

Next, after running the actual command, check whether the last-modified date of the files has changed using stat:

  • If they have, yet the contents haven't changed, the implication is that sed has processed the files, but the regex in the s function call didn't match anything.

It is conceivable that older GNU sed versions don't work properly when combining -i (in-place editing) with multiple file operands (though I couldn't find anything in the GNU sed release notes).
To rule that out, invoke sed once for each file:

If you still want to use xargs, add -n 1:

 find /myprojects -type f -name '*.cpp' -print0 | 
   xargs -0 -n 1 sed -i 's/previousword/newword/g'

To use find's -exec action, see chepner's answer.


With a GNU sed version that does support updating of multiple files with the -i option - which is the case as of at least v4.2.2 - the best formulation of your command is (note the quoted *.cpp argument to prevent premature expansion by the shell, and the use of terminator + to only invoke sed once):

find /myprojects -type f -name '*.cpp' -exec sed -i 's/previousword/newword/g' '{}' +
Community
  • 1
  • 1
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • The `echo` hint helped me to solve the issue, that was related to files structure. It indeed works !!! Thanks for helping!!! – Mendes Nov 10 '15 at 14:51