Using a loop to rename a group of files is one of the things that I'm often thankful for. Sure, if I have 20, 40, 100 files, I could rename them all manually, but that's some tedious work.
Fortunately, we have PowerShell.
~/Desktop/Images> Get-ChildItem | Select-Object -Property Name
Name
----
DSC_4745.jpg
DSC_4791.jpg
DSC_4830.jpg
DSC_4947.jpg
DSC_4973.jpg
DSC_5035.jpg
DSC_5043.jpg
DSC_5187.jpg
DSC_5217.jpg
DSC_5296.jpg
DSC_5312.jpg
I have 11 files here (I didn't count them; I used code.Count with the grouping operator). These images are from a photo shoot I did with my nephew for his senior photos, all cropped to be printed at 5x7 aspect ratio. Since I'm also printing a different set cropped for 8x10 aspect ratio, I want to make sure I keep the files sorted correctly. To help me remember which files are the different crop aspect ratios, I want to add Crop-5x7- to the beginning of each of these files.
At first, this seemed like a simple task:
~/Desktop/Images> Get-ChildItem | foreach { Rename-Item -Path $_.Name -NewName "Crop-5x7-$_.Name" }
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Rename-Item: Cannot rename the specified target, because it represents a path or device name.
Well, that's not right.
To troubleshoot, I did another loop, this time displaying the $_.Name property for each file with Write-Host:
~/Desktop/Images> Get-ChildItem | foreach { Write-Host "Crop-5x7-$_.Name" }
Crop-5x7-/Users/jwright/Desktop/Images/DSC_4745.jpg.Name
Crop-5x7-/Users/jwright/Desktop/Images/DSC_4791.jpg.Name
Crop-5x7-/Users/jwright/Desktop/Images/DSC_4830.jpg.Name
Crop-5x7-/Users/jwright/Desktop/Images/DSC_4947.jpg.Name
Crop-5x7-/Users/jwright/Desktop/Images/DSC_4973.jpg.Name
Crop-5x7-/Users/jwright/Desktop/Images/DSC_5035.jpg.Name
Crop-5x7-/Users/jwright/Desktop/Images/DSC_5043.jpg.Name
Crop-5x7-/Users/jwright/Desktop/Images/DSC_5187.jpg.Name
Crop-5x7-/Users/jwright/Desktop/Images/DSC_5217.jpg.Name
Crop-5x7-/Users/jwright/Desktop/Images/DSC_5296.jpg.Name
Crop-5x7-/Users/jwright/Desktop/Images/DSC_5312.jpg.Name
Well, that's not what I wanted at all.
In the -NewName argument I specified "Crop-5x7-$.Name", thinking that PowerShell would expand the variable \(_.Name as part of the new string. However, PowerShell expanded \) first (which resolved to /Users/jwright/Desktop/Images/DSC_4745.jpg), then added the prefix Crop-5x7- and treated .Name as a suffix.
Subexpressions to the Rescue
What I want is for PowerShell to expand the $_.Name property first, then add the prefix Crop-5x7- to the beginning of the file name. To force PowerShell to expand the string in this manner, I need to use a subexpression.
In PowerShell, subexpressions are specified using a leading dollar sign, then parenthesis: $(). A subexpression tells PowerShell to execute this first and then treat the result like a variable:
~/Desktop/Images> Get-ChildItem | foreach { Write-Host "Crop-5x7-$($_.Name)" }
Crop-5x7-DSC_4745.jpg
Crop-5x7-DSC_4791.jpg
Crop-5x7-DSC_4830.jpg
Crop-5x7-DSC_4947.jpg
Crop-5x7-DSC_4973.jpg
Crop-5x7-DSC_5035.jpg
Crop-5x7-DSC_5043.jpg
Crop-5x7-DSC_5187.jpg
Crop-5x7-DSC_5217.jpg
Crop-5x7-DSC_5296.jpg
Crop-5x7-DSC_5312.jpg
Perfect! The subexpression is evaluated first, and then the result is incorporated as a string into the file name with the specified prefix. Incorporating that into the Rename-Item loop gives me this:
~/Desktop/Images> Get-ChildItem | foreach { Rename-Item -Path $_.Name -NewName "Crop-5x7-$($_.Name)" }
~/Desktop/Images> Get-ChildItem | Select-Object -Property Name
Name
----
Crop-5x7-DSC_4745.jpg
Crop-5x7-DSC_4791.jpg
Crop-5x7-DSC_4830.jpg
Crop-5x7-DSC_4947.jpg
Crop-5x7-DSC_4973.jpg
Crop-5x7-DSC_5035.jpg
Crop-5x7-DSC_5043.jpg
Crop-5x7-DSC_5187.jpg
Crop-5x7-DSC_5217.jpg
Crop-5x7-DSC_5296.jpg
Crop-5x7-DSC_5312.jpg
🙌
There and Back Again
After printing the images, I wanted to rename them back to the original DSC_NNNN.jpg file names again. I could have re-exported them from Lightroom, but I took the opportunity to experiment more with using PowerShell to rename groups of files.
Strings in PowerShell have a method called Replace(). We can look at the methods using an empty string and Get-Member:
~/Desktop/Images> '' | Get-Member -MemberType Method | Select-String 'Replace\('
string Replace(string oldValue, string newValue, bool ignoreCase, cultureinfo culture), string Replace(string oldValue, string
newValue, System.StringComparison comparisonType), string Replace(char oldChar, char newChar), string Replace(string oldValue, string
newValue)
Using Replace(), I can remove the Crop-5x7- part of the file names to rename back to the original names. First, let's test it out to make sure we get what we need:
~/Desktop/Images> Get-ChildItem | foreach { Write-Host "$_.Name.Replace('Crop-5x7-','')" }
/Users/jwright/Desktop/Images/Crop-5x7-DSC_4745.jpg.Name.Replace('Crop-5x7-','')
/Users/jwright/Desktop/Images/Crop-5x7-DSC_4791.jpg.Name.Replace('Crop-5x7-','')
/Users/jwright/Desktop/Images/Crop-5x7-DSC_4830.jpg.Name.Replace('Crop-5x7-','')
/Users/jwright/Desktop/Images/Crop-5x7-DSC_4947.jpg.Name.Replace('Crop-5x7-','')
/Users/jwright/Desktop/Images/Crop-5x7-DSC_4973.jpg.Name.Replace('Crop-5x7-','')
/Users/jwright/Desktop/Images/Crop-5x7-DSC_5035.jpg.Name.Replace('Crop-5x7-','')
/Users/jwright/Desktop/Images/Crop-5x7-DSC_5043.jpg.Name.Replace('Crop-5x7-','')
/Users/jwright/Desktop/Images/Crop-5x7-DSC_5187.jpg.Name.Replace('Crop-5x7-','')
/Users/jwright/Desktop/Images/Crop-5x7-DSC_5217.jpg.Name.Replace('Crop-5x7-','')
/Users/jwright/Desktop/Images/Crop-5x7-DSC_5296.jpg.Name.Replace('Crop-5x7-','')
/Users/jwright/Desktop/Images/Crop-5x7-DSC_5312.jpg.Name.Replace('Crop-5x7-','')
🤦♂️
We need to use the subexpression operator again here, to get PowerShell to evaluate the $_.Name.Replace() statement, returning the modified file name as a string:
~/Desktop/Images> Get-ChildItem | foreach { Write-Host "$($_.Name.Replace('Crop-5x7-',''))" }
DSC_4745.jpg
DSC_4791.jpg
DSC_4830.jpg
DSC_4947.jpg
DSC_4973.jpg
DSC_5035.jpg
DSC_5043.jpg
DSC_5187.jpg
DSC_5217.jpg
DSC_5296.jpg
DSC_5312.jpg
Perfect! By adding $( to the beginning of the file name, and a trailing ) to the end, we can apply the subexpression operator, producing the desired file name. Next, we can use Rename-Item in the foreach loop to rename the files:
~/Desktop/Images> Get-ChildItem | foreach { Rename-Item -Path $_.Name -NewName $($_.Name.Replace('Crop-5x7-','')) }
~/Desktop/Images> Get-ChildItem | Select-Object -Property Name
Name
----
DSC_4745.jpg
DSC_4791.jpg
DSC_4830.jpg
DSC_4947.jpg
DSC_4973.jpg
DSC_5035.jpg
DSC_5043.jpg
DSC_5187.jpg
DSC_5217.jpg
DSC_5296.jpg
DSC_5312.jpg
Conclusion
In this article we looked at the practical skill of using PowerShell to rename a collection of files. First we looked at renaming several files to include a prefix string, and how we need to use the PowerShell subexpression operator $() to expand the desired properties when referencing the file names with Rename-Item. Next we looked at returning the files to their original names, using the subexpression operator again but also with the string method Replace() to strip the leading file name prefixes.
With PowerShell, and some understanding of how expressions are evaluated, it's not too difficult to automate the process of renaming files. Hopefully you'll remember this article and swoop in and help someone less fortunate, next time you see them manually renaming hundreds of files.
-Joshua Wright
Return to Getting Started With PowerShell
Joshua Wright is the author of SANS SEC504: Hacker Tools, Techniques, and Incident Handling, a faculty fellow for the SANS Institute, and a senior technical director at Counter Hack.