I created a C:\test
directory with files 20200712_123456.jpg
and 20200712_123457.jpg
. Passing -PassThru
to Rename-Item
illustrates what's going on...
PS> Get-ChildItem -Path 'C:\test' -Filter '*.jpg' | % { $_ | Rename-Item -NewName {$_.BaseName.insert(4,'_').insert(7,'_') + $_.Extension} -PassThru }
Directory: C:\test
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 7/15/2020 7:05 PM 0 2020_07_12_123456.jpg
-a---- 7/15/2020 7:05 PM 0 2020_07_12_123457.jpg
-a---- 7/15/2020 7:05 PM 0 2020__0_7_12_123456.jpg
Renaming two files produces three FileInfo
objects! Note that the results are the same when simplifying the pipeline to...
Get-ChildItem -Path 'C:\test' -Filter '*.jpg' |
Rename-Item -NewName {$_.BaseName.insert(4,'_').insert(7,'_') + $_.Extension}
I think what happens when Get-ChildItem
enumerates the directory is this...
- It encounters
20200712_123456.jpg
and renames it to 2020_07_12_123456.jpg
.
- It encounters
20200712_123457.jpg
and renames it to 2020_07_12_123457.jpg
.
- Because
2020_07_12_123456.jpg
(formerly 20200712_123456.jpg
) was inserted after 20200712_123457.jpg
, it encounters it again and applies a second pass of renaming to 2020__0_7_12_123456.jpg
.
Why doesn't this game of renaming leap-frop carry on indefinitely? That I don't yet know.
Decoupling the directory enumeration (Get-ChildItem
) from the modification of the directory listing (Rename-Item
) produces the expected result for me...
PS> $filesToRename = Get-ChildItem -Path 'C:\test' -Filter '*.jpg'
PS> $filesToRename | ForEach-Object { $_ | Rename-Item -NewName {$_.BaseName.insert(4,'_').insert(7,'_') + $_.Extension} -PassThru }
Directory: C:\test
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 7/15/2020 7:05 PM 0 2020_07_12_123456.jpg
-a---- 7/15/2020 7:05 PM 0 2020_07_12_123457.jpg
I do observe that the original pipeline works as-expected with PowerShell 7.0.2, so it seems this is an issue with Get-ChildItem
that has been addressed since 5.1. The note at the bottom of this answer confirms it has as a side-effect of another change...
... [explicitly buffering the complete directory contents] is technically no longer necessary in PowerShell [Core] 6+ (it is necessary in Windows PowerShell (5.1-)), because Get-ChildItem
is implemented in a way that always internally collects info about all files up front, across platforms, because it sorts them by name, which is inherently only possible after all names have been collected.
Buffering the results of Get-ChildItem
before using them isn't ideal, so an alternative way to perform this underscore insertion would be to attempt to parse and, upon success, reformat the file name...
PS> Get-ChildItem -Path 'C:\test' -Filter '*.jpg' |
ForEach-Object {
$timeFromName = [DateTime]::MinValue
if ([DateTime]::TryParseExact($_.BaseName, 'yyyyMMdd_hhmmss', $null, [Globalization.DateTimeStyles]::None, [Ref] $timeFromName))
{
$_ | Rename-Item -NewName {$timeFromName.ToString('yyyy_MM_dd_hhmmss') + $_.Extension} -PassThru
}
}
Directory: C:\test
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 7/15/2020 7:05 PM 0 2020_07_12_123456.jpg
-a---- 7/15/2020 7:05 PM 0 2020_07_12_123457.jpg
Not only does this prevent the double-rename-in-a-single-pipeline described above, it would allow you to re-run the script any time you want and it would only rename the files that exactly match the original format.