Skip to content

Instantly share code, notes, and snippets.

@mogenson
Created November 10, 2025 14:53
Show Gist options
  • Select an option

  • Save mogenson/fab15803aec58988fbf377ba1b68c1f8 to your computer and use it in GitHub Desktop.

Select an option

Save mogenson/fab15803aec58988fbf377ba1b68c1f8 to your computer and use it in GitHub Desktop.
Recursive find and replace Perl script
#!/usr/bin/env perl
use strict;
use warnings;
use File::Find;
use Cwd 'abs_path';
use File::Basename 'basename';
# --- Configuration ---
# Set to 1 to see files that were scanned but not changed.
my $verbose = 0;
# --- End Configuration ---
# 1. Argument Validation
if (@ARGV != 2) {
my $script_name = basename($0);
print "Usage: $script_name <regex> <replacement>\n";
print "Error: Expected 2 arguments, got " . scalar(@ARGV) . ".\n";
exit 1;
}
my ($regex, $replacement) = @ARGV;
# 2. Setup
my $start_dir = ".";
my $files_changed = 0;
my $files_scanned = 0;
# Get the full, absolute path to this script so we can skip it.
my $script_abs_path = eval { abs_path($0) } || $0;
print "Starting recursive replace in $start_dir\n";
print " Regex: '$regex'\n";
print " Replacement: '$replacement'\n";
print "--------------------------------\n";
# 3. Find files and process them
# find() takes a reference to the processing sub and the dirs to search.
find(\&wanted, $start_dir);
# 5. Summary
print "--------------------------------\n";
print "Done.\n";
print "Scanned $files_scanned text files.\n";
print "Changed $files_changed files.\n";
exit 0;
# 4. The processing subroutine for File::Find
# This sub is called for every file and directory.
# The full path is in $File::Find::name.
sub wanted {
# Prune .git directories to avoid corrupting the repo
if ($_ eq ".git" && -d) {
$File::Find::prune = 1;
return;
}
# We only want to process text files.
# The -f and -T operators work on $_, which is the current file name.
# If the current item is a directory, -f is false, and we do nothing.
# File::Find then automatically handles recursing into the directory.
if (-f && -T) {
# Use $File::Find::name for messages and checks, but $_ for open/write
# because File::Find chdirs into the file's directory.
my $file_path = $File::Find::name;
# Skip this script itself!
my $file_abs_path = eval { abs_path($file_path) } || $file_path;
return if $file_abs_path eq $script_abs_path;
$files_scanned++;
# Read the entire file into memory ("slurp" mode)
local $/; # Sets input record separator to undef
open my $fh, '<', $_ or do {
warn "[ERROR] Could not read $file_path: $!";
return; # Skip this file
};
my $content = <$fh>;
close $fh;
# Perform the replacement.
# We use {} as delimiters for s/// so the regex can contain slashes.
# The 'g' flag means replace all occurrences globally.
# The s/// operator returns the number of substitutions made.
my $matches = ($content =~ s{$regex}{$replacement}g);
# Write the content back *only* if a change was made.
# This preserves file timestamps and avoids unnecessary I/O.
if ($matches > 0) {
open my $out_fh, '>', $_ or do {
warn "[ERROR] Could not write to $file_path: $!";
return; # Skip this file
};
print $out_fh $content;
close $out_fh;
print "[CHANGED] $file_path\n";
$files_changed++;
} else {
if ($verbose) {
print "[Scanned] $file_path (no changes)\n";
}
}
}
}
__END__
=head1 NAME
replace.pl - Recursively find and replace text in files.
=head1 SYNOPSIS
./replace.pl <regex> <replacement>
=head1 DESCRIPTION
This script recursively searches the current directory ('.') for all files.
For each file that is identified as a text file (using Perl's -T operator),
it reads the content, performs a global regex substitution, and writes the
content back to the file *only if* changes were made.
It safely handles regexes and replacements containing special characters
(like slashes) by using non-standard delimiters in the s/// operator.
It will skip binary files and itself.
=head1 EXAMPLES
# Simple rename
./replace.pl "old_function_name" "new_function_name"
# Regex with capture groups
# This turns "User(admin)" into "LegacyUser[admin]"
./replace.pl "User\((\w+)\)" "LegacyUser[$1]"
=head1 DEPENDENCIES
This script uses only core Perl modules (File::Find, Cwd, File::Basename).
=cut
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment