开发者

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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜