Recursive search and replace usind Perl in cmd (Windows)
I am using this command to search and replace a string with another in the command prompt:
perl -pi -i.bak -e "s/Mohan/Sitaram/g" ab.txt
This replaces Mohan
with Sitaram
in the file ab.txt
in the current directory.
However I want to replace all occurrences of Mohan
with Sitaram
in all .txt
files in all the sub-directories (recursively). Using *.txt
instead of ab.txt
doesn’t work. Regular expressions work otherwise as I have downloaded the regex packages for Wi开发者_JAVA百科ndows. It doesn’t work only for this command saying
E:\>perl -pi -e "s/Sitaram/Mohan/g" *.txt
Can't open *.txt: Invalid argument.
Is there any way to fix this? A different command perhaps?
find . -name "*.txt" | xargs perl -p -i -e "s/Sitaram/Mohan/g"
find
is used to search all *.txt files recursively.
xargs
is used to build and execute command lines from standard input.
Windows solution
On Windows, a command can be executed for multiple files using the forfiles
command. The /s
option tells it to search directories recursively.
forfiles /s /m *.txt /c "perl -pi -e s/Sitaram/Mohan/g @path"
If starting the search from other than the current working directory is desired, supply /p path\to\start
.
Unix solution
On Unix, there is a more generic command than forfiles
called xargs
, which passes lines of its standard input as arguments to the given command. Directories are searched recursively for .txt
files using the find
command.
find . -name '*.txt' | xargs perl -pi -e 's/Sitaram/Mohan/g'
Platform-independent solution
You can also code both the search for files and string replacement in Perl. The File::Find
core module can help with that. (Core module = distributed with the interpreter.)
perl -MFile::Find -e 'find(sub{…}, ".")'
However the Perl code will be longer and I don’t want to spend time writing it. Implement the sub yourself using info from the File::Find
manpage linked above. It should test if the file name ends with .txt
and is not a directory, create its backup and rewrite the original file by the changed version of the backup.
The quoting will differ on Windows – perhaps writing the script into a file will be the only sane solution there.
Problems with OP’s original approach
In Unix shell, glob patterns (e.g. *.txt
) are expanded by the shell, whereas Windows cmd leaves them untouched and passes them right to the program being invoked. It is its job to handle them. Perl cannot do that obviously.
Second problem is that even under Unix, globbing would not work as desired. *.txt
are all .txt
files in the current directory, not including those in subdirectories and their subdirectories…
If you're going to bother with Perl, why not simply go all out and write a (short) Perl program to do this for you?
This way, you're not passing it off between the shell and your program, and you have something that's more universal and can run on multiple operating systems.
#!/usr/bin/env perl <-- Not needed for Windows, but tradition rules
use strict;
use warnings;
use feature qw(say);
use autodie; # Turns file operations into exception based programming
use File::Find; # Your friend
use File::Copy; # For the "move" command
# You could use Getopt::Long, but let's go with this for now:
# Usage = mungestrings.pl <from> <to> [<dir>]
# Default dir is current
#
my $from_string = shift;
my $to_string = shift;
my $directory = shift;
$from_string = quotemeta $from_string; # If you don't want to use regular expressions
$directory = "." if not defined $directory;
#
# Find the files you want to operate on
#
my @files;
find(
sub {
return unless -f; # Files only
return unless /\.txt$/ # Name must end in ".txt"
push @files, $File::Find::name;
},
$directory
);
#
# Now let's go through those files and replace the contents
#
for my $file ( @files ) {
open my $input_fh, "<", $file;
open my $output_fh, ">" "$file.tmp";
for my $line ( <$input_fh> ) {
$line =~ s/$from_string/$to_string/g;
print ${output_fh} $line;
}
#
# Contents been replaced move temp file over original
#
close $input_fh;
close $output_fh;
move "$file.tmp", $file;
}
I use File::Find
to gather all of the files that I want to modify in my @files
array. I could have put the whole thing inside the find
subroutine:
find(\&wanted, $directory);
sub wanted {
return unless -f;
return unless /\.txt/;
#
# Here: open the file for reading, open output and move the lines over
#
...
}
The whole program is in the wanted
subroutine this way. It's more efficient because I'm now replacing as I am finding the files. No need to go through first, find the files, then do the replacement. However, it strikes me as bad design.
You can also slurp your entire file into an array without looping through it at first:
open my $input_fh, "<", $file;
@input_file = <$input_fh>;
Now, you can use grep
to check to see if there's anything that needs to be replaced:
if ( grep { $from_string } @input_file ) {
# Open an output file, and do the loop to replace the text
}
else {
# String not here. Just close up the input file
# and don't bother with writing a new one and moving it over
}
This is more efficient (no need to do a replace unless that file has the string you're looking for). However, it takes up memory (the entire file must be in memory at one time), and don't let that one line fool you. The entire file is still read into that array one line at a time as if you did an entire loop.
The File::Find
and File::Copy
are standard Perl modules, so all Perl installations have them.
精彩评论