1

I would like to rename all .jpg files in a specific folder in format yyyymmdd_SequenceNumber to include an underscore after yyyy and also after mm. So, for example, a file named 20200712_123456.jpg would be renamed 2020_07_12_123456.jpg. Should work for all .jpg files in the folder, and optimally for all .mp4 files as well. The following code ALMOST works, except that the very first .jpg filename is changed to 2020__07_12_123456.jpg (note the double underscores). All other files are renamed just fine.

Get-ChildItem -Path 'C:\test' -Filter '*.jpg' | 
  ForEach-Object { $_ | Rename-Item -NewName {$_.BaseName.insert(4,'_').insert(7,'_') + $_.Extension}}

The strange thing is that the following code did work, but I've read some articles that says results using the -Include could be unpredictable:

Get-ChildItem -Path 'C:\test\*' -Include *.jpg, *.mp4 | 
  ForEach-Object { $_ | Rename-Item -NewName {$_.BaseName.insert(4,'_').insert(7,'_') + $_.Extension}}

I'm more of a batch script kinda guy, but am trying my hand at Powershell. Thanks in advance for your help.

Olaf
  • 4,690
  • 2
  • 15
  • 23
DuxChux
  • 15
  • 5
  • 1
    You're using `-NewName { ... }` instead of `-NewName "$( ... )"` or `-NewName ( ... )`. That would be passing a `ScriptBlock`, not a `String`, to that parameter. I've never seen that before and would question if that even works, but...evidently it almost does. – Lance U. Matthews Jul 15 '20 at 23:50
  • Did the first file already have an underscore at position 4 before the rename? – AdminOfThings Jul 15 '20 at 23:51
  • 1
    @BACON that is the delay-script bind, which supposedly works for any parameter that accepts pipeline input. – AdminOfThings Jul 15 '20 at 23:53
  • @AdminOfThings Yes, just found that in [`about_Script_Blocks`](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_script_blocks?view=powershell-7#using-delay-bind-script-blocks-with-parameters). It doesn't seem like it's providing any advantage here, though, is it? If I understand it, it would allow `Get-ChildItem` to be piped directly into `Rename-Item`, but inside `ForEach-Object` it's the same as `"$( ... )"` or `( ... )`. – Lance U. Matthews Jul 15 '20 at 23:54
  • For me, in PowerShell 5.1 the first file, `20200712_123456.jpg`, gets renamed to `2020__0_7_12_123456.jpg` (_two_ extra underscores) whereas in 7.0.2 it correctly becomes `2020_07_12_123456.jpg`. If I change to `Get-ChildItem ... | ForEach-Object { $_.BaseName.insert(4,'_').insert(7,'_') + $_.Extension }` it outputs the correct file name on 5.1. `Get-ChildItem ... | Rename-Item -NewName {$_.BaseName.insert(4,'_').insert(7,'_') + $_.Extension}` behaves the same for both versions: extra underscores on 5.1, but not 7.0.2. – Lance U. Matthews Jul 16 '20 at 00:17
  • Thanks @Bacon for all your investigative work. I installed Powershell 7 and, as you indicated, the issue was resolved. I'm rethinking my requirements though, to not rename the file, but check if there's a folder named yyyy_mm (first 6 characters of filename), create it if it doesn't exist, then simply move the file into the appropriate folder. Is that something I can get help with? Thanks again... – DuxChux Jul 17 '20 at 11:46
  • You can certainly get help for that on Stack Overflow. However, since that changes the fundamental nature of a question that's already received a couple answers, I would ask that in a new question. – Lance U. Matthews Jul 17 '20 at 16:05
  • Absolutely understood. Will do. Thanks again for your feedback on my initial question. – DuxChux Jul 17 '20 at 17:14
  • @AdminOfThings In response to your question, the filename had an underscore only after position 8: '20200712_123456.jpg' – DuxChux Jul 17 '20 at 17:43
  • Remember that [if your question receives answers](https://stackoverflow.com/help/someone-answers), you can [accept](https://stackoverflow.com/help/accepted-answer) one that you found the most helpful, if any. With sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you'll also be able to [vote up](https://stackoverflow.com/help/privileges/vote-up) or [down](https://stackoverflow.com/help/privileges/vote-down) on the helpfulness of answers to any question as well as the questions themselves. – Lance U. Matthews Jul 17 '20 at 18:21

2 Answers2

1

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.

Lance U. Matthews
  • 15,725
  • 6
  • 48
  • 68
0

I would use this:

Get-ChildItem -Path 'C:\test\*' -Include *.jpg, *.mp4 | 
  ForEach-Object { 
    $NewName = $_.BaseName.insert(4,'_').insert(7,'_') + $_.Extension
    Rename-Item -Path "$_.Fullname" -Newname "$Newname"
}
Wasif
  • 14,755
  • 3
  • 14
  • 34