powershellcsv

Replace Words in File Names Based on Imported CSV


I have a series of .pdf files where the name contains an old file number, and I'd like to convert it to the new file number. However, I only want to change the file number portion, I don't want to rename the whole document. There also isn't consistency in the document names, so I can't just put the entire path in a .csv and do them that way.

For example, based on the following .csv:

oldname,newname
A12345,A98765
A00645,A39502

The following files:

A12345 Termination Letter.pdf
A00645 Correspondence.pdf

Would become:

A98765 Termination Letter.pdf
A39502 Correspondence

I currently use the following script to remove or replace single words in the names of files in a given directory (in this example, I replace - Copy with nothing):

$path = "C:\path"
$filter = '*.pdf'
get-childitem -path $path -filter $filter | 
 rename-item -newname {$_.name -replace '- Copy',''}

I'm trying to work off this to build something that will work the same way, but pull the replacement words from a .csv so I can do multiple replacements at once. I'm not sure if I'm just not using the right syntax in my searching, but I keep finding ways to rename whole documents, change words in a .csv, etc. I'm essentially just looking to do a find and replace.


Solution

  • A way to do it is using a regex OR pattern where you have all the oldname and then you use replacement with a match evaluator or replacement with a scriptblock (same thing just that the latter only works in newer versions of PowerShell) where you can get the newname from a give match (.Value).

    A little example of how the logic goes:

    # this is a dictionary where you have `oldname` and `newname`
    $map = @{
        A12345 = 'A98765'
        A00645 = 'A39502'
    }
    
    # the OR pattern
    $replacePattern = [regex]::new(
        'A12345|A00645',
        [System.Text.RegularExpressions.RegexOptions] 'Compiled, IgnoreCase')
    
    # the replacement logic
    'A12345 Termination Letter.pdf', 'A00645 Correspondence.pdf' |
        ForEach-Object { $replacePattern.Replace($_, { $map[$args[0].Value] }) }
    
    # Outputs:
    # A98765 Termination Letter.pdf
    # A39502 Correspondence.pdf
    

    In the actual code, the $map is constructed dynamically but the rest of the code is similar, however there should be a check for if the regex pattern matches the file name otherwise you would be trying to rename a file with the same name and that fails in older versions:

    # this dynamically creates dictionary with `oldname` and `newname`
    $map = @{}
    Import-Csv 'the\path\toMyRenamingFiles.csv' |
        ForEach-Object { $map[$_.oldname] = $_.newname }
    
    # this creates an OR pattern like 'A12345|A00645|...|...|etc'
    $replacePattern = [regex]::new(
        $map.Keys.ForEach({ [regex]::Escape($_) }) -join '|',
        [System.Text.RegularExpressions.RegexOptions] 'Compiled, IgnoreCase')
    
    $path = 'C:\path'
    $filter = '*.pdf'
    Get-ChildItem -Path $path -Filter $filter |
        Where-Object { $replacePattern.IsMatch($_.Name) } |
        Rename-Item -NewName { $replacePattern.Replace($_.Name, { $map[$args[0].Value] }) }