Created
November 10, 2025 14:53
-
-
Save mogenson/fab15803aec58988fbf377ba1b68c1f8 to your computer and use it in GitHub Desktop.
Recursive find and replace Perl script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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