
Bulk updates to files using Powershell
Sometimes you just have to do some bulk updating on some files.
For reasons I today needed to apply some bulk updates to a lot of code files. A colleague did this last week, using sublime text to make the required changes within files, and then I helped him put together the necessary changes to rename files in Powershell. This week it has been my turn to do something very similar. Except the problem is, whilst I know I can do these changes in Powershell the exact commands always elude me and I need to work them out each time. So now is the time to document how I did it today for posterity.
My first step was to apply some file renames, but only to specific files. If you’re here your probably aware of Get-ChildItem -Filter "your-filter"
. My issue is that actually I only want to change files named *Your-Filter*
and ignore *your-filter*
. (Yes, I’m aware Windows isn’t case sensitive, but NTFS is….) Regexes are case sensitive, and Powershell does regex by default. Except Get-ChildItem
doesn’t. What we can do though is pipe the files through to another Cmdlet and filter them there. For this we can use the Where-Object
Cmdlet, which is aliased to ?
. The last part then is to use Rename-Item
to do the rename. And so:
Get-ChildItem -Recurse | ?{ $_.Name -match "Your-Filter" } | %{ Rename-Item $_.PSPath -NewName ($_.Name -replace "Your-Filter","My-File") }
Breaking this down:
Get-ChildItem -Recurse
gets all of the items under the current directory and sub-directories.?{ $_.Name -match "Your-Filter" }
?
is an alias for theWhere-Object
Cmdlet,$_
is a pipeline placeholder,-match
invokes a regex match. The whole construct filters the incoming files based on name using the provided regex.($_.Name -replace "Your-Filter","My-File")
is an expression that replacesYour-Filter
withMy-File
and returns the new string.%{ Rename-Item $_.PSPath -NewName ($_.Name -replace "Your-Filter","My-File") }
%
is an alias for theForEach-Object
Cmdlet, looping over each object in the pipeline. For each item, we then call theRename-Item
Cmdlet, passing it the itemsPSPath
property. The new name is derived from the expression shown in step 3.
The next step was selectively change the contents of files. Theoretically this can be done using Get-Content
and Set-Content
. But there are two problems:
Set-Content
seems to always update the specified file. This is problematic because only some of the files need changing, and I don’t want a commit touching every single file.Get-Content
(by default) gets the file contents as an array of strings. This makes it hard to determine if we can skip updating the current file.
The key (for me) to resolving this was discovering that Get-Content
has a -Raw
flag that retrieves the contents as a single string. Using this I was able to come up with the following multi-line script:
$files = Get-ChildItem -Recurse | ?{ !($_.PSIsContainer) }
foreach($file in $files) {
$contents = Get-Content -Path $file.PSPath -Raw
if($contents -match "Your_Filter") {
Set-Content -Value ($contents -replace "Your_Filter", "My_Filter") -Path $file.PSPath
}
}
Breaking this down:
$files = Get-ChildItem -Recurse | ?{ !($_.PSIsContainer) }
get the list of items in the current directory and sub-directories, and then filters out any directories (as you can’t alter the contents of a directory in this way…). The list of files is then assigned to the$files
variable.foreach($file in $files)
loops over the files in the$files
variable. This could have been written as%{ ... }
with$_
replacing$file
.$contents = Get-Content -Path $file.PSPath -Raw
gets the contents of the current file as a single string.if($contents -match "Your_Filter")
as above-match
invokes a regex comparison on the variable.Set-Content -Value ($contents -replace "Your_Filter", "My_Filter") -Path $file.PSPath
when executed this step will cause the textYour_Filter
to be replaced withMy_Filter
and then saved to the file.
The above could (probably) be re-written as a one-liner just using pipes and without variables, and I leave that as an exercise for you the reader :) .