Find/Replace in files recursively but touch only files with matches - regex

I would like to quickly search and replace with or without regexp in files recursively. In addition, I need to search only in specific files and I do not want to touch the files that do not match my search_pattern otherwise git will think all the parsed files were modified (it what happens with find . --exec sed).
I tried many solutions that I found on internet using find, grep, sed or ack but I don't think they are really good to match specific files only.
Eventually I wrote this perl script:
#!/bin/perl
use strict;
use warnings;
use File::Find;
my $search_pattern = $ARGV[0];
my $replace_pattern = $ARGV[1];
my $file_pattern = $ARGV[2];
my $do_replace = 0;
sub process {
return unless -f;
return unless /(.+)[.](c|h|inc|asm|mac|def|ldf|rst)$/;
open F, $_ or print "couldn't open $_\n" && return;
my $file = $_;
my $i = 0;
while (<F>) {
if (m/($search_pattern)/o) {$i++};
}
close F;
if ($do_replace and $i)
{
printf "found $i occurence(s) of $search_pattern in $file\n";
open F, "+>".$file or print "couldn't open $file\n" && return;
while (<F>)
{
s/($search_pattern)/($replace_pattern)/g;
print F;
}
close F;
}
}
find(\&process, ".");
My question is:
Is there any better solution like this one below (which not exists) ?
`repaint -n/(.+)[.](c|h|inc|asm|mac|def|ldf|rst)$/ s/search/replacement/g .`
Subsidiary questions:
How's my perl script ? Not too bad ? Do I really need to reopen every files that match my search_pattern ?
How people deal with this trivial task ? Almost every good text editor have a "Search and Replace in files" feature, but not vim. How vim users can do this ?
Edit:
I also tried this script ff.pl with ff | xargs perl -pi -e 's/foo/bar/g' but it doesnt work as I expected. It created a backup .bak even though I didn't give anything after the -pi. It seems it is the normal behaviour within cygwin but with this I cannot really use perl -pi -e
#!/bin/perl
use strict;
use warnings;
use File::Find;
use File::Basename;
my $ext = $ARGV[0];
sub process {
return unless -f;
return unless /\.(c|h|inc|asm|mac|def|ldf|rst)$/;
print $File::Find::name."\n" ;
}
find(\&process, ".");
Reedit:
I finally came across this solution (under cygwin I need to remove the backup files)
find . | egrep '\.(c|h|asm|inc)$' | xargs perl -pi.winsucks -e 's/<search>/<replace>/g'
find . | egrep '\.(c|h|asm|inc)\.winsucks$' | xargs rm

The following is a cleaned up version of your code.
Always include use strict; and use warnings at the top of EVERY perl script. If you're doing file processing, include use autodie; as well.
Go ahead and slurp the entire file. That way you only have to read and write optionally write it once.
Consider using File::Find::Rule for cases like this. Your implmentation using File::Find works, and actually is probably the preferred module in this case, but I like the interface for the latter.
I removed the capture groups from the regex. In ones in the RHS were a bug, and the ones in the LHS were superfluous.
And the code:
use strict;
use warnings;
use autodie;
use File::Find;
my $search_pattern = $ARGV[0];
my $replace_pattern = $ARGV[1];
my $file_pattern = $ARGV[2];
my $do_replace = 0;
sub process {
return if !-f;
return if !/[.](?:c|h|inc|asm|mac|def|ldf|rst)$/;
my $data = do {
open my $fh, '<', $_;
local $/;
<$fh>;
};
my $count = $data =~ s/$search_pattern/$replace_pattern/g
or return;
print "found $count occurence(s) of $search_pattern in $_\n";
return if !$do_replace;
open my $fh, '>', $_;
print $fh $data;
close $fh;
}
find(\&process, ".");

Not bad, but several minor notes:
$do_replace is always 0 so it will not replace
in-place open F, "+>" will not work on cygwin + windows
m/($search_pattern)/o /o is good, () is not needed.
$file_pattern is ignored, you overwrite it with your own
s/($search_pattern)/($replace_pattern)/g;
() is unneeded and will actually disturb a counter in the $replace_pattern
/(.+)[.](c|h|inc|asm|mac|def|ldf|rst)$/ should be written as
/\.(c|h|inc|asm|mac|def|ldf|rst)$/ and maybe /i also
Do I really need to reopen every files that match my search_pattern ?
You don't do.
Have no idea about vim, I use emacs, which has several method to accomplish this.

What's wrong with the following command?
:grep foo **/*.{foo,bar,baz}
:cw
It won't cause any problem with any VCS and is pretty basic Vimming.
You are right that Vim doesn't come with a dedicated "Search and Replace in files" feature but there are plugins for that.

why not just:
grep 'pat' -rl *|xargs sed -i 's/pat/rep/g'
or I didn't understand the Q right?

I suggest find2perl if it doesn't work out of the box, you can tweak the code it generates:
find2perl /tmp \! -name ".*?\.(c|h|inc|asm|mac|def|ldf|rst)$" -exec "sed -e s/aaa/bbb/g {}"
it will print the following code to stdout:
#! /usr/bin/perl -w
eval 'exec /usr/bin/perl -S $0 ${1+"$#"}'
if 0; #$running_under_some_shell
use strict;
use File::Find ();
# Set the variable $File::Find::dont_use_nlink if you're using AFS,
# since AFS cheats.
# for the convenience of &wanted calls, including -eval statements:
use vars qw/*name *dir *prune/;
*name = *File::Find::name;
*dir = *File::Find::dir;
*prune = *File::Find::prune;
sub wanted;
sub doexec ($#);
use Cwd ();
my $cwd = Cwd::cwd();
# Traverse desired filesystems
File::Find::find({wanted => \&wanted}, '/tmp');
exit;
sub wanted {
my ($dev,$ino,$mode,$nlink,$uid,$gid);
(($dev,$ino,$mode,$nlink,$uid,$gid) = lstat($_)) &&
! /^\..*.?\\.\(c|h|inc|asm|mac|def|ldf|rst\)\$\z/s &&
doexec(0, 'sed -e s/aaa/bbb/g {}');
}
sub doexec ($#) {
my $ok = shift;
my #command = #_; # copy so we don't try to s/// aliases to constants
for my $word (#command)
{ $word =~ s#{}#$name#g }
if ($ok) {
my $old = select(STDOUT);
$| = 1;
print "#command";
select($old);
return 0 unless <STDIN> =~ /^y/;
}
chdir $cwd; #sigh
system #command;
chdir $File::Find::dir;
return !$?;
}
If you want to execute, you can pipe it to perl:
find2perl /tmp \! -name ".*?\.(c|h|inc|asm|mac|def|ldf|rst)$" -exec "sed -e s/aaa/bbb/g" | perl

You can try this plugin for Vim:
https://github.com/skwp/greplace.vim
Basically, it allows you to type in a search phases (with/without regex) and ask you for the files to search in.

Related

How to replace string in a file with Perl in script (not in command line)

I want to replace a string in a file. Of course I can use
perl -pi -e 's/pattern/replacement/g' file
but I want to do it with a script.
Is there any other way to do that instead of system("perl -pi -e s/pattern/replacement/g' file")?
-i takes advantage that you can still read an unlinked filehandle, you can see the code it uses in perlrun. Do the same thing yourself.
use strict;
use warnings;
use autodie;
sub rewrite_file {
my $file = shift;
# You can still read from $in after the unlink, the underlying
# data in $file will remain until the filehandle is closed.
# The unlink ensures $in and $out will point at different data.
open my $in, "<", $file;
unlink $file;
# This creates a new file with the same name but points at
# different data.
open my $out, ">", $file;
return ($in, $out);
}
my($in, $out) = rewrite_file($in, $out);
# Read from $in, write to $out as normal.
while(my $line = <$in>) {
$line =~ s/foo/bar/g;
print $out $line;
}
You can duplicate what Perl does with the -i switch easily enough.
{
local ($^I, #ARGV) = ("", 'file');
while (<>) { s/foo/bar/; print; }
}
You can try the below simple method. See if it suits your requirement best.
use strict;
use warnings;
# Get file to process
my ($file, $pattern, $replacement) = #ARGV;
# Read file
open my $FH, "<", $file or die "Unable to open $file for read exited $? $!";
chomp (my #lines = <$FH>);
close $FH;
# Parse and replace text in same file
open $FH, ">", $file or die "Unable to open $file for write exited $? $!";
for (#lines){
print {$FH} $_ if (s/$pattern/$replacement/g);
}
close $FH;
1;
file.txt:
Hi Java, This is Java Programming.
Execution:
D:\swadhi\perl>perl module.pl file.txt Java Source
file.txt
Hi Source, This is Source Programming.
You can handle the use case in the question without recreating the -i flag's functionality or creating throwaway variables. Add the flag to the shebang of a Perl script and read STDIN:
#!/usr/bin/env perl -i
while (<>) {
s/pattern/replacement/g;
print;
}
Usage: save the script, make it executable (with chmod +x), and run
path/to/the/regex-script test.txt
(or regex-script test.txt if the script is saved to a directory in your $PATH.)
Going beyond the question:
If you need to run multiple sequential replacements, that's
#!/usr/bin/env perl -i
while (<>) {
s/pattern/replacement/g;
s/pattern2/replacement2/g;
print;
}
As in the question's example, the source file will not be backed up. Exactly like in an -e oneliner, you can back up to file.<backupExtension> by adding a backupExtension to the -i flag. For example,
#!/usr/bin/env perl -i.bak
You can use
sed 's/pattern/replacement/g' file > /tmp/file$$ && mv /tmp/file$$ file
Some sed versions support the -i command, so you won't need a tmpfile. The -i option will make the temp file and move for you, basicly it is the same solution.
Another solution (Solaris/AIX) can be using a here construction in combination with vi:
vi file 2>&1 >/dev/null <#
1,$ s/pattern/replacement/g
:wq
#
I do not like the vi solution. When your pattern has a / or another special character, it will be hard debugging what went wrong. When replacement is given by a shell variable, you might want to check the contents first.

Script or command to increment number in file name inside file

I have a file in which we have entries in following format. I would like to increment the numbers in file names inside this file. So some_v1.png will become some_v2.png. Is there a way with regex OR command line utility to achieve this.
Following is example file (file.config) with file entries as string.
something/some_v1.png
something/some_v4.png
something/some_v3.png
This looks like a great match for awk's "split" function:
awk '{n=split($0,a,"[1-9][0-9]*",s);for(i=1;i<n;++i)printf "%s%d",a[i],s[i]+1;print a[n]}'
The perl one-liner you already found also works great, with one exception: files with leading-zero numbers will lose the zeroes. Here is a fix for that using the magical auto-increment:
perl -pe 's/(\d+)/++($a=$1)/eg'
If you want to rename a bunch of files I'd use an auxiliary directory and a test to see if there is an actual file to rename.
mkdir aux
for i in {1..7} ; do
j=$($i + 1)
[ -f something/some_v${i}.png ] && mv something/some_v${i}.png aux/some_v${j}.png
done
mv aux/* something
rmdir aux
The use of a fixed name for the auxiliary directory could not stand a security review for repeated use in a dynamic production environment but I think it's fine for a one shot use in a controlled environment.
In perl:
#!/usr/bin/env perl
use strict;
use warnings;
foreach
my $filename (
sort { $b =~ s/.*(\d+).*/$1/r <=> $a =~ s/.*(\d+).*/$1/r }
glob "something/some_v*.png" )
{
chomp $filename;
if ( my ($vnum) = $filename =~ m/(\d+)\.png/ ) {
print "mv $filename ", $filename =~ s|\d+\.png|++$vnum.".png"|re,
"\n";
}
}
Note - sorting numerically, to ensure that you're never replacing 5 with 4, before you've renamed 5.

Script to add line with part of the pattern used to find the line

I'd like to parse all *.php files, and for each line like
$res = $DB -> query($queryVar);
I need to get:
file_put_contents('php://stderr', print_r($queryVar, TRUE));
$res = $DB -> query($queryVar);
The name of the variable $queryVar may change! I need to get it from the code!
My initial idea:
find -not -path "*/\." -name "*.php" -type f -print0 | xargs -0 sed -i 's,SOMETHING,SOMETHING,'
but it seems to be not possible to get the name of the query variable with sed.
I also started looking at Perl: Perl: append a line after the last line that match a pattern (but incrementing part of the pattern)
But I was able to do only this:
perl -pe 's/(-> query\(.*\))/AAAAA $1 AAAAA\n$1/' < filename.php
With 2 problems: I get the result on standard output, I need something like sed to edit the original file, as I will call it from find | xargs and anyway I get the whole found line and not only the variable:
$res = $DB AAAAA -> query( $SQL) AAAAA
-> query( $SQL);
Given a file named filename.php, you can run the following command:
perl -pi -e 's/^(.+-> query\((.+?)\).*)$/file_put_contents\("php:\/\/stderr", print_r\($2, TRUE\)\);\n$1/;' filename.php
It will update the file in-place with the substitution you intended to perform.
You can use perl's -i flag to edit the file in place.
To only capture the query variable you need to add a capture group within the () part, as follows:
perl -i -pe 's/^(.*-> query\((.*)\);)$/inserted_code_here($2);\n$1/' x.php
Then replace inserted_code_here with whatever you want to put on the line before the query call.
You can use perl like sed. But really, by doing so you throw away a lot of its potential as a language. I couldn't quite tell from your question - is $queryVar a literal, or is it a variable you need to replace?
Why not try this:
#!/usr/bin/perl
use strict;
use warnings;
use File::Find;
sub process_php {
next unless m/\.php$/;
open( my $input, "<", $File::Find::name ) or warn $!;
open( my $output, ">", $File::Find::name . ".new" )
or warn $!;
while ( my $line = <$input> ) {
my ($query_id) = ( $line =~ m/-> query\((.*)\))/ );
if ($query_id) {
print {$output} "file_put_contents('php://stderr', print_r(",
$query_id, " TRUE));\n";
}
print {$output} $line;
}
close($input);
close($output);
}
find( \&process_php, "/path/to/php/files" );
This will:
search all the '*.php' files under the directory path.
traverse them looking for your string.
If it exists, add a new line just before it.
write a '.new' file, with the new content (Once you're happy this works, you can swap 'em over).

Bulk renaming files with bash and Perl based on file name

I'm looking to bulk rename files in the current directory only and remove certain strings from the end of file names.
Sample:
foo-bar-(ab-4529111094).txt
foo-bar-foo-bar-(ab-189534).txt
foo-bar-foo-bar-bar-(ab-24937932201).txt
the output should look like this:
foo-bar.txt
foo-bar-foo-bar.txt
foo-bar-foo-bar-bar.txt
I want to remove the string -(ab-2492201) at the end of each file name
knowing that the digits can vary in length.
A Perl regex is preferred over modules and without using any utilities and for bash oneliner command is highly preferred.
How to accomplish that in both Perl and Bash Shell on Linux? interested to know both solutions.
Try:
$ rename 's/-\(ab-\d+\)(?=\.txt$)//' *.txt
There's a rename command written in Perl. Its first argument is Perl code describing how to transform a filename. You could use the same s/// command in your own Perl program or one-liner.
If that doesn't work, try prename instead of rename; there's a different, non-Perl-based, rename command installed on some systems, in which case the Perl one may be called prename.
Using Perl Regex to Rename Files
With find, perl, and xargs, you could use this one-liner
find . -type f | perl -pe 'print $_; s/input/output/' | xargs -n2 mv
Results without calling mv should just be
OldName NewName
OldName NewName
OldName NewName
How does it work?
find . -type f outputs file paths (or file names...you control what gets processed by regex here!)
-p prints file paths to be processed by regex, -e executes inline script
print $_ prints the original file name first (independent of -p)
-n2 prints two elements per line
mv gets the input of the previous line
In bash, you could write something like:
for file in *-\(ab-[0-9]*\)*; do
newfile="${file/-(ab-[0-9]*)/}"
mv "$file" "$newfile"
done
When you say under the current directory, do you mean in the current directory, or anywhere in or beaneath the current directory and its descendants?
File::Find is a simple way to do the latter, and is a core module so won't need installing. Like so:
use strict;
use warnings;
use autodie;
use File::Find;
find(\&rename, '.');
sub rename {
return unless -f;
my $newname = $_;
return unless $newname =~ s/-\(ab-[0-9]+\)(\.txt)$/$1/i;
print "rename $_, $newname\n";
}
Update
This program will rename all the files with the given filename pattern only within the current directory.
Note that the initial open loop is there only to create sample files for renaming.
use strict;
use warnings;
use autodie;
open my $fh, '>', $_ for qw(
foo-bar-(ab-4529111094).txt
foo-bar-foo-bar-(ab-189534).txt
foo-bar-foo-bar-bar-(ab-24937932201).txt
);
for (glob '*.txt') {
next unless -f;
my $newname = $_;
next unless $newname =~ s/-\(ab-[0-9]+\)(\.txt)$/$1/i;
print "rename $_, $newname\n";
rename $_, $newname;
}
output
rename foo-bar-(ab-4529111094).txt, foo-bar.txt
rename foo-bar-foo-bar-(ab-189534).txt, foo-bar-foo-bar.txt
rename foo-bar-foo-bar-bar-(ab-24937932201).txt, foo-bar-foo-bar-bar.txt
A simpler, shorter (better ? :) ) rename regex :
rename 's#-\(.*?\)##' foo*.txt
check this:
ls -1 | nawk '/foo-bar-/{old=$0;gsub(/-\(.*\)/,"",$0);system("mv \""old"\" "$0)}'
> ls -1 foo*
foo-bar-(ab-4529111094).txt
foo-bar-foo-bar-(ab-189534).txt
foo-bar-foo-bar-bar-(ab-24937932201).txt
> ls -1 | nawk '/foo-bar-/{old=$0;gsub(/-\(.*\)/,"",$0);system("mv \""old"\" "$0)}'
> ls -1 foo*
foo-bar-foo-bar-bar.txt
foo-bar-foo-bar.txt
foo-bar.txt
>
For detailed explanation check here
Another way using just perl:
perl -E'for (<*.*>){ ($new = $_) =~ s/(^.+?)(-\(.+)(\..*$)/$1$3/; say $_." -> ".$new}'
(say ... is nice for testing, just replace it with rename $_,$new or rename($_,$new) )
<*.*> read every file in the current directory
($new = $_) =~ saves the following substitution in $new and leaves $_ as intact
(^.+?) save this match in $1 and non-greedy match from the beginning until...
(-\(.+) the sequence "-( ...anything..." is found. (this match would be saved in $2)
(\..*$) save everything from the last "." (period) before the end ($) of the line until and including the end of the line -> into $3
substitute the match with the string generated from $1$3
( you could also do it for a specific directory with perl -E'for (</tmp/my/directory/*.*>){ .....

How do I replace an arbitrary number of backreferences in sed or Perl? (for obfuscating mailto)

I'm looking for a way to obfuscate mailtos in the source code of a web site. I'd like to go from this:
href="mailto:president#whitehouse.gov"
To this:
href="" onmouseover="this.href='mai'+'lto:'+'pre'+'sid'+'ent'+'#wh'+'ite'+'hou'+'se.'+'gov'"</code>
I'm probably going to go with a PHP solution instead, like this (that way I only have to globally replace the entire mailto, and the source on my end will look better), but I spent too much time looking at sed and Perl and now I can't stop thinking about how this could be done! Any ideas?
Update: Based heavily on eclark's solution, I eventually came up with this:
#!/usr/bin/env perl -pi
if (/href="mailto/i) {
my $start = (length $`) +6;
my $len = index($_,'"',$start)-$start;
substr($_,$start,$len,'" onmouseover="this.href=' .
join('+',map qq{'$_'}, substr($_,$start,$len) =~ /(.{1,3})/g));
}
#!/usr/bin/perl
use strict; use warnings;
my $s = 'mailto:president#whitehouse.gov';
my $obfuscated = join('+' => map qq{'$_'}, $s =~ /(.{1,3})/g );
print $obfuscated, "\n";
Output:
'mai'+'lto'+':pr'+'esi'+'den'+'t#w'+'hit'+'eho'+'use'+'.go'+'v'
Note that 'lto: is four characters, whereas it looks like you want three character groups.
Building on Sinan's idea, here's a short perl script that will process a file line by line.
#!/usr/bin/env perl -p
my $start = index($_,'href="') +6;
my $len = index($_,'"',$start)-$start;
substr($_,$start,$len+1,'" onmouseover="this.href=' .
join('+',map qq{'$_'}, substr($_,$start,$len) =~ /(.{1,3})/g)
);
If you're going to use it, make sure you have your old files committed to source control and change the -p option to -i, which will rewrite a file in place.
Is this close enough?
use strict;
use warnings;
my $old = 'href="mailto:president#whitehouse.gov"';
$old =~ s/href="(.*)"/$1/;
my $new = join '+', map { qq('$_') } grep { length $_ } split /(.{3})/, $old;
$new = qq(href=""\nonmouseover="this.href=$new\n");
print "$new\n";
__END__
href=""
onmouseover="this.href='mai'+'lto'+':pr'+'esi'+'den'+'t#w'+'hit'+'eho'+'use'+'.go'+'v'
"
Just an example.
$ echo $s
href="mailto:president#whitehouse.gov"
$ echo $s | sed 's|\(...\)|\1+|g' | sed 's/hre+f=\"/href="" onmouseover="this.href=/'
href="" onmouseover="this.href=+mai+lto+:pr+esi+den+t#w+hit+eho+use+.go+v"
Ack! Thppfft! I offer you this hairball:
s='href="mailto:president#whitehouse.gov"'
echo "$s" | sed -n 's/=/=\x22\x22\n/;
h;
s/\n.*//;
x;
s/[^\n]*\n//;
s/"//g;
s/\(...\)/\x27&\x27+/g;
s/.*/onmouseover=\x22this.href=&\x22/;
x;
G;
s/\n//2;
s/+\([^\x22]\{1,2\}\)\x22$/+\x27\1\x27\x22/;
s/+\x22$/\x22/;
p'