1

I would like to use Vim to find certain string and replace it with another. For every replacements, it should ask for confirmation similar to what %s/foo/replace/gc does for a single file in Vim.

What have I tried?

  1. sed: It doesn't do interactive replacements.
  2. One of the comments in the following this link suggests vim -esnc '%s/foo/bar/g|:wq' file.txt. I tried vim -esnc '%s/foo/bar/gc|:wq' file.txt (used gc instead of g). Now the terminal gets stuck.
  3. Emacs xah-find-replace package. Unfortunately it didn't do interactive replacements as promised in the link.
Drew
  • 29,895
  • 7
  • 74
  • 104
vineeshvs
  • 479
  • 7
  • 32

3 Answers3

2

If you are looking for an interactive mode of replacement, it is easier to do it with vim.

vim -c '%s/PATTERN/REPLACEMENT/gc' -c 'wq' FILENAME

The stuck terminal in your case is due to piping the save command to the replacement string, as it does not allow the interactive mode to come in to action. And it is not a stuck terminal, if you type "yes" and press enter it should still show you the expected result.

In case multiple files are involved which is spread across multiple subdirectories, using find command with for loop will help as mentioned below:

for FILENAME in `find DIRECTORYPATH -type f -name *.txt` 
do 
    vim -c '%s/PATTERN/REPLACEMENT/gc' -c 'wq' $FILENAME
done
LogicIO
  • 627
  • 7
  • 15
  • Hi, @LogicIO I just tried "vim -c '%s/PATTERN/REPLACEMENT/gc' -c 'wq' *.txt" to do replacements in a.txt and b.txt. It does the replacements in a.txt and asks me whether to quit or not. Both y/n options leads me to the case where it doesn't do replacements in b.txt. – vineeshvs Apr 20 '19 at 15:33
  • 1
    If you are trying to do it on multiple files, better put it in a for loop, something like this :- for filename in *.txt; do vim -c '%s/PATTERN/REPLACEMENT/gc' -c 'wq' "$filename"; done – LogicIO Apr 20 '19 at 15:37
  • Tried that. Kept it in run.sh and executed it. The terminal just flashed once and went to the next line. No replacements happened. Missing something here? – vineeshvs Apr 20 '19 at 15:41
  • 1
    strange. it works perfectly for me. ex: ~]# cat test*.txt hi hi ; for i in test*.txt ; do vim -c '%s/hi/hello/gc' -c 'wq' $i; done ; cat test*.txt hello hello for me I have two files here and the terminal flashes twice asking for confirmation and I have to press "y" – LogicIO Apr 20 '19 at 15:48
  • Works now. The flashing of terminal was because there was no matching string to replace. My bad. How can we get it working for all files in sub-directories as well? – vineeshvs Apr 20 '19 at 15:58
  • 1
    Great! for sub-directories, you should be using something like find command with for loop to find the files and put it in loop. I would do something like this - for i in `find DIRECTORYPATH -type f -name *.txt` ; do OPERATION ; done – LogicIO Apr 20 '19 at 16:04
  • Could you edit your answer and post a final code considering find DIRECTORYPATH -type f -name *.txt ; do OPERATION ; done. This will help me accept your solution as the answer. – vineeshvs Apr 20 '19 at 16:06
  • I am getting the error "find: ‘DIRECTORYPATH’: No such file or directory" for your code in the updated answer. Also we need to have "shopt -s globstar", "**/*.txt" and in the code to ensure that it traverses all the files in the sub-directories. I really appreciate the effort you have put. But I need to accept the answer of phd here as it works. – vineeshvs Apr 21 '19 at 12:27
  • 1
    DIRECTORYPATH has to be replaced with the parent directory path in which you are trying to find the files. In that case you wont need to set shopt. However, happy that you got it working somehow. – LogicIO Apr 22 '19 at 04:02
2

Combining :argdo with the substitute command would be the recommended way to do this.

You can populate the args by either opening all the files vim *.txt or manually populate this after opening vim using the command:

:args `find . -type f -name '*.txt'`

Now set hidden using the command:

:set hidden

this is required so that you're not prompted to save the file when switching from one buffer to the other. Refer, :h hidden for more information.

Now use the substitute command like you're used to, prefixing the argdo to perform this for every file in the argslist

:silent argdo %s/pattern/replace/gec

The silent is optional and just mutes the reporting. The e flag is to stop reporting the error no matches found message in some of the buffers

Now after replace, you can write the changes using the following command

:argdo update

This will write buffers that were modified only.

Harish
  • 1,433
  • 9
  • 21
1

In bash turn on double star to list all files in all subdirectories:

shopt -s globstar

Now start vim once with all files and run the substitute command for all files, then save and exit:

vim -c 'set nomore' -c 'argdo %s/foo/bar/gc' -c xa **/*.txt
phd
  • 82,685
  • 13
  • 120
  • 165
  • Just tried "vim -c 'set nomore' -c 'argdo %s/foo/bar/gc' -c xa *.txt". The terminal just flashes once and went to the next line. No replacements happened in the file. – vineeshvs Apr 20 '19 at 15:43
  • 1
    Are there `*.txt` files in the current directory? `*.txt` works only in the current directory. – phd Apr 20 '19 at 16:08
  • I understand. I guess your code works only for the items in a sub directory. I need to do replacements in the current directory, inside sub-directories and all the directories inside those sub-directories and so on. Could you modify your answer accordingly. – vineeshvs Apr 20 '19 at 16:11
  • 1
    `shopt -s globstar` + `**/*.txt` works in the current directory and all subdirectories recursively. Try `echo **/*.txt` before `vim` to see the list of files. – phd Apr 20 '19 at 16:25