Interactive search and replace from shell
Search and repl开发者_如何学运维ace over multiple files is difficult in my editor. There are plenty of tricks that can be done with find
, xargs
and sed
/awk
incluing search-and replace in multiple files. But somehow I couldn't find a way to make this interactive. Do you know a way to do that?
KISS principle:
vim
:args `ls`
:argdo %s#SEARCH#REPLACE#gec |update
First character afer %s is used as separator
I would add these modifications to Dillon's answer:
The -le
option should be added to the grep
command.
vim `find . -name '*.c' -exec grep -le '\<junk\>' {} \;`
Then you are in Vim, but you don't have the opportunity to choose what to replace, add c
option at the end for interactive replacements and bufdo
at the beginning for walking through every file:
:bufdo %s/junk/rubbish/gce
Later you save all your work:
:bufdo wq!
From jhvaras answer, I've made this bash command to quickly search and replace (to add to .bashrc
):
replace () {
if [ $# -lt 2 ]
then
echo "Recursive, interactive text replacement"
echo "Usage: replace text replacement"
return
fi
vim -u NONE -c ":execute ':argdo %s/$1/$2/gc | update' | :q" $(ag $1 -l)
}
It's used as follows:
~$ replace some_text some_new_text
It uses ag
to search in advance, as it's probably faster than letting vim
do the work, but you can probably substitute anything else you like. It also calls vim
with no plugins for maximum speed, and after it has finished all substitutions it automatically quits and goes back to the shell.
Just use Vim.
Start by using find to make a list of all files which need changing like so.
vim `find . -name '*.c' -exec grep junk {} \;`
That starts vim with all the .c
files containing the string junk
. Now, in vim make your change to the first file:
:%s/junk/rubbish/g
And then type :wEnter:nEnter for the next file.
Now you need to repeat the editing process. If it is only one substitute command and just a few files, then type :UpEnter to repeat the substitute command.
But if there are a lot, you should use map
to create a couple of kestroke macros. The first macro would do the substitute command(s) and the second macro would do the w
and n
commands to save changes and get the next file.
I have used this (and older variants with vi) for the past 30 years. In particular, when you encode the two parts of this as two keystroke macros, you can run it very fast because it is easy to hold down the control key, and type a couple of letters, for instance R Y, over and over again. It is faster than the time it takes to write a fully automated script and since this is simple to reproduce, you can do it on any machine that you happen to be working on.
A UNIX way to do this with grep, xargs, and vi/vim:
- Make an alias
This will help avoiding long commands when you want to exclude certain things from grep.
alias ge='grep --exclude=tags --exclude=TAGS --exclude-dir=.git --exclude-dir=build --exclude-dir=Build --exclude-dir=classes --exclude-dir=target--exclude-dir=Libraries --exclude=*.log --exclude=*~ --exclude=*.min.js -rOInE $*'
- Perform the search and check your file list
Command:
ge -l 'foo' .
- Perform the search again, telling vim to replace interactively on each file
Command:
ge -l 'foo' . | xargs -L 1 -o vim -c '%s/foo/bar/gc'
Explanation of the command:
The part till the pipe is the search command repeated
After the pipe, we call xargs to launch vim on each file
"-L 1" tells xargs to launch the command on each line instead of combining all lines in a single command. If you prefer to launch vim on all files at once, remove "-L 1" from xargs.
"-o" tells xargs to use the actual terminal rather than /dev/null
You have two options for this command:
a) Launch vim on all files at once
Advantages:
- Command is easy to cancel: just quit vim
Disadvantages:
You have to switch to each buffer in turn and re-run the "%s" command on it
If you have a lot of files, this can make vim consume a lot of memory (sometimes as much as a full-blown IDE!)
b) Launch vim in turn for each file
Advantages:
Everything is automated. Vim is launched and performs the search command
Light on memory
Disadvantages:
- It's hard to cancel: quitting vim will just make xargs open the next one
I prefer variant b) as it's automated and more UNIX-like. With careful planification - inspecting the files list first, and maybe running the command on sub-directories to reduce scope - it is very nice to use.
General advantages:
It can be achieved with standard UNIX tools, available on virtually every platform
It's nice to use
General disadvantages:
You have to type the search expression twice
You might hit issues with bash escaping on complex search/replace expressions
Here is an enhancement of @jhvaras's answer if you find that you frequently want to do this, or if you only want to search files under version control.
function SvnProjectSubstitute(replacement)
execute "args `grep -sl '" . @/ . "' $(svn --recursive list)`; echo -n ''"
argdo execute "substitute//" . a:replacement . "/gc"
endfunction
command -nargs=1 -complete=file Gsubstitute call SvnProjectSubstitute(<f-args>)
Notes:
- As I have formulated it here, it only searches those files which Subversion has under version control from the current directory recursing down. You can off course generalize that or fit it to a different version management system according to your needs.
- The pattern which is used is the last pattern that was searched for. Once again, if you do not like this, you can fit it to your needs.
Gsubstitute
is for "global substitute". You can call the command whatever you want but I like this because:Gs
seems to fit stylistically into Vim.- This is a nice solution if you have a very large number of files, because it takes the onus off of Vim for searching through them all (i.e. it would probably solve this guy's problem too).
- The do-nothing echo at the end and the
-s
passed togrep
are to cover up an errors thatgrep
encounters.grep
gives a non-zero return code even if it does find good results.
Example use:
/OldClassName
:Gs NewClassName
One reason I like using the previous search pattern is it allows you to quickly give a class an entirely different name by placing your cursor on the class, doing a * or a #, and then your :Gs
command, which saves you from having to type out the whole search string.
you can try this tool for find and replace in intercative mode inner a shell
https://sourceforge.net/projects/isartool/
No X server is required, and if you want it's recursive in directory using the option -r.
Ciao
I couldn't get Mario's answer to work, but if I use xargs it works.
Also, most times that I want to do search and replace in multiple files, I want to change an alphanumeric ID from one thing to another, such as renaming a variable or a class, etc. For that, it's very helpful to add a negative lookbehind and negative lookahead to disallow word characters before or after what you're replacing, and for that grep needs the P flag to use Perl regex:
vim `find . -name '*.cpp' -o -name '*.hpp' | xargs grep -Ple '(?<!\w)FIND(?!\w)'`
:bufdo %s/FIND/REPLACE/gce
Of course, it's helpful to wrap this up in a script so that you don't have to re-type it every time. Here's a start:
#!/bin/bash
if [[ $# < 2 ]]; then
echo "Usage: search-replace <search> <replace>"
exit 1
fi
search="$1"
replace="$2"
files=`find . -name '*.cpp' -o -name '*.hpp' | xargs grep -Ple "(?<!\w)$search(?!\w)"`
if [[ -z "$files" ]]; then
echo "No matching .cpp or .hpp files."
exit 1
fi
# The stuff surrounding $search is the negative lookahead/lookbehind
vim -c "set hidden | bufdo %s/\(\w\)\@<!$search\(\w\)\@!/$replace/gce" $files
Something that's still not working gracefully for me with Mario's solution is that at the end of the first file it says:
Error detected while processing command line:
E37: No write since last change (add ! to override)
This prevents the search and replace from flowing to the next file. It appears that "set hidden" fixes this, and so I've used that in my above script.
One remaining issue is that after doing search and replace on the first file, it seems to process that first file a second time, undoing your initial replaces. If anyone knows why that is happening, let me know.
精彩评论