Search and Replace from terminal, but with confirmation

6,805

Solution 1

How about Vim?

vim '+%s/set/bar/gc' some_file
  • + is used to run a command after the file has loaded.
  • % runs the command over the whole buffer (file).
  • gc are flags to :substitute, g for acting over all expressions in the line and c for confirming each substitution.

You can't actually prevent Vim from opening up the file, but you can automate saving and exiting:

vim '+bufdo %s/set/bar/gc | up' '+q' some_file another_file
  • bufdo runs the command over each buffer. This includes the part after |, so changes to each buffer is saved (up).
  • q quits, which exits Vim since we are now at the last buffer.

Solution 2

You can use combination of find and its -ok command. This command is the same as the -exec command but ask the user first before execution of each specified command. If the user agrees, run the command. Otherwise just return false.

from man find:

-ok command ;
      Like  -exec  but ask the user first. If the user agrees, run the command. 
      Otherwise just return false. If the command is run, its standard input is 
      redirected from /dev/null.

So, you can use the command as follows:

$ find ./ -name filename -ok sed 's/foo/bar/' {} \;
< sed ... filename > ?

this will prompt the user as shown in the second line above.

If you enter y then the sed replacement command will be executed and your replacement will be made. If you enter n then the -ok command ignores the sed command.


If you want to do this "prompted" find and replace across all files within a directory, use the command as following:

$ find /path/to/directory -type f -ok sed 's/foo/bar/' {} \;

Solution 3

I would just write a little Perl script:

#!/usr/bin/env perl
use strict;
use warnings;

my $pat="$ARGV[0]";
my $replacement=$ARGV[1];
my $file="$ARGV[2]";

## Open the input file
open(my $fh, "$file");

## Read the file line by line
while (<$fh>) {
    ## If this line matches the pattern
    if (/$pat/) {
        ## Print the current line
        print STDERR "Line $. : $_";
        ## Prompt the user for an action
        print STDERR "Substitute $pat with $replacement [y,n]?\n";
        ## Read the user's answer
        my $response=<STDIN>;
        ## Remove trailing newline
        chomp($response);
        ## If the answer is y or Y, make the replacement.
        ## All other responses will be ignored. 
        if ($response eq 'Y' || $response eq 'y') {
            s/$pat/$replacement/g;
        }    
    }
    ## Print the current line. Note that this will 
    ## happen irrespective of whether a replacement occured.
    print;
}

Save the file as ~/bin/replace.pl, make it executable with chmod a+x ~/bin/replace.pl and run it with the pattern to match as the first argument, the replacement as the second and the file name as the third:

replace.pl foo bar file > newfile

To run it on multiple files, for example all *.txt files, wrap it in a bash loop:

for file in *txt; do 
    replace.pl foo bar "$file" > "$file".new
done

And to edit the file "in place":

for file in *txt; do 
    tmp=$(mktemp)
    replace.pl foo bar "$file" > "$tmp" && mv "$tmp" "$file"
done

Solution 4

You can combine muru's suggestion to use vim with αғsнιη's suggestion to use find (which has the advantage of recursing into subdirectories), for example:

find ./ -type f -exec vim -c '%s/originalstring/replacementstring/gc' -c 'wq' {} \;
Share:
6,805

Related videos on Youtube

random_forest_fanatic
Author by

random_forest_fanatic

Updated on September 18, 2022

Comments

  • random_forest_fanatic
    random_forest_fanatic over 1 year

    In many text editors, you can do a search and replace and be given the option to inspect each found case. I'd like to be able to do something similar to this at the command line in Ubuntu. I know sed offers the ability to find and replace strings across multiple files, but is there anyway to have each replacement confirmed by the user?

    Ideally, I'd like a solution that would allow me to do this "prompted" find and replace across all files within a directory.

  • random_forest_fanatic
    random_forest_fanatic about 9 years
    Thanks for the answer! Is there any way to use this approach to process multiple files at once? And, is there a way to run this so that it doesn't open up the file and require me to exit it each time?
  • random_forest_fanatic
    random_forest_fanatic about 9 years
    The '+wqa' command seems to work great, but I think it's closing the buffer and preventing additional files to be searched (after the first file with a match is processed). Thanks for the help!
  • muru
    muru about 9 years
    @random_forest_fanatic the search happens through all files for me.
  • random_forest_fanatic
    random_forest_fanatic about 9 years
    You're getting the command above to work for replacing strings across two files? Everything I try seems to quit once I've made one replacement...
  • random_forest_fanatic
    random_forest_fanatic about 9 years
    This seems to be the closest to what I'm looking for! But, I'd really like to be able to look at each replacement within each file, rather than having to accept/reject replacement for all matches in a file.
  • αғsнιη
    αғsнιη about 9 years
    @random_forest_fanatic Updated answer.
  • muru
    muru about 9 years
    @random_forest_fanatic Hmm. You could combine the find with the vim way to get the best of both worlds.
  • muru
    muru over 6 years
    @random_forest_fanatic rather too late now, but looks like I had some personal config which saved edits automatically. Without saving changes after the :s command, bufdo can't switch buffers and errors out. I fixed the multi-file command so it should now work fine.
  • random_forest_fanatic
    random_forest_fanatic over 6 years
    I can't even remember what I needed this for, but this solution works. Thanks!
  • Hunaphu
    Hunaphu over 2 years
    Perfect. For people finding this answer: it opens a vim session for each file where the string is found and if you press y, the replacement is done, the file is written and you move on to next. Note! find ./*.ext is good if you want to avoid replacement in binary files.