Perl: Find Replace Text in Directory 📜

By Xah Lee. Date: . Last updated: .

Here is a Perl script that do find and replace.

# -*- coding: utf-8 -*-
# perl

=pod

File name: xah_find_replace.pl

Description: This script does find and replace on a given dir.

Features:
• Allow multiple Find Replace string pairs.
• Find strings can be set to regex or literal.
• Filter file by file name regex.
• Backup copies of original files is made at a user specified folder that preserves all folder structures of original folder.
• A report is generated that indicates which files has been changed, how many changes, and total number of files changed.
• Changed FILES retain their own/group/permissions settings.

usage:
Edit the parameters section then run the script

homepage: http://xahlee.info/perl/perl_find_replace_in_dir.html
Author: Xah Lee

Created: 2000-02
Version: 2025-07-08

=cut

use utf8;
use strict;

use File::Find;
use File::Path;
use File::Copy;
use Data::Dumper;

# HHHH------------------------------
# parameters

my $inputDirPath = "c:/Users/xah/xx/";

# backup dir path. if no exit, will be created
my $backupDirPath = "c:/Users/xah/xx_back-2025-07-08-NSkKH";

my $filenameRegex = qr{\.html$};

# 1 for true, 0 for false
my $writeToFile = 1;

# find replace string pairs
my %findReplaceH = (
    "find1" => "rep1",
    "find2" => "rep2",
);

# $useRegexQ has values 1 or 0. If 1, inteprets the pairs in %findReplaceH
# to be regex.
my $useRegexQ = 0;

# in bytes. larger files will be skipped
my $fileSizeLimit = 2000 * 1000;

# HHHH------------------------------
# globals

my $fileSeparator = "==file=sep=M6bcX=====================================\n";

# remove ending slash
$inputDirPath  =~ s[/$][];
$backupDirPath =~ s[/$][];

$inputDirPath =~ m[/(\w+)$];
my $previousDir = $`;                         # e.g. '/home/joe'
my $lastDir     = $1;                         # e.g. 'public_html'
my $backupRoot  = $backupDirPath . '/' . $1;  # e.g. '/tmp/joe_back/public_html'

my $refLargeFiles         = [];
my $totalFileChangedCount = 0;

# HHHH------------------------------
# subroutines

# fileFilterQ($fullFilePath) return true if file is desired.
sub fileFilterQ ($) {
    my $fileName = $_[0];

    if ( ( -s $fileName ) > $fileSizeLimit ) {
        push( @$refLargeFiles, $fileName );
        return 0;
    }
    if ( $fileName =~ $filenameRegex ) {

        # print "checking: $fileName\n";
        return 1;
    }

##        if (-d $fileName) {return 0;}; # directory
##        if (not (-T $fileName)) {return 0;}; # not text file

    return 0;
}

# go through each file, accumulate a hash.
sub doFile {
    my $currentFile     = $File::Find::name;
    my $currentDir      = $File::Find::dir;
    my $currentFileName = $_;

    if ( not fileFilterQ($currentFile) ) {
        return 1;
    }

    # Read whole file
    if ( not( open FILE, "<$currentFile" ) ) {
        die(
            "Error opening file:
$!"
        );
    }
    my $wholeFileString;
    { local $/ = undef; $wholeFileString = <FILE>; };
    if ( not( close(FILE) ) ) { die("Error closing file: $!"); }

    # do the replacement.
    my $replaceCount = 0;

    foreach my $key1 ( keys %findReplaceH ) {
        my $pattern = ( $useRegexQ ? $key1 : quotemeta($key1) );
        $replaceCount = $replaceCount +
          ( $wholeFileString =~ s/$pattern/$findReplaceH{$key1}/g );
    }

    if ( $replaceCount > 0 ) {
        $totalFileChangedCount++;

        if ($writeToFile) {

            # do backup
            # make a directory in the backup path, make a backup copy.
            my $pathAdd = $currentDir;
            $pathAdd =~ s[$inputDirPath][];
            mkpath( "$backupRoot/$pathAdd", 0, 0777 );
            copy( $currentFile, "$backupRoot/$pathAdd/$currentFileName" )
              or die "error: file copying file failed on $currentFile\n$!";

            # write to the original
            # get the file mode.
            my ( $mode, $uid, $gid ) = ( stat($currentFile) )[ 2, 4, 5 ];

            # write out a new file.
            if ( not( open OUTFILE, ">$currentFile" ) ) {
                die("Error opening file: $!");
            }
            print OUTFILE $wholeFileString;
            if ( not( close(OUTFILE) ) ) { die("Error closing file: $!"); }

            # set the file mode.
            chmod( $mode, $currentFile );
            chown( $uid, $gid, $currentFile );
        }

        # print "-----out77311-------------------------------\n";
        print $fileSeparator;
        print "$replaceCount replacements made at\n";
        print "$currentFile\n";
    }

}

# HHHH------------------------------

# main

find( \&doFile, $inputDirPath );

print "--------------------------------------------\n\n\n";
print "Total of $totalFileChangedCount files changed.\n";

if ( scalar @$refLargeFiles > 0 ) {
    print "The following large files are skipped:\n";
    print Dumper($refLargeFiles);
}

Sample output

perl find replace output 2025-07-08 18909
perl find replace output 2025-07-08 18909

I've been using this script from 2000 to 2005.

Find Replace Scripts