Automatically batch renaming files according to a formula including parent folder name using Powershell
Asked Answered
L

3

1

How can I batch rename files in powershell using the following code:

$nr=1;Get-ChildItem -Filter *.jpg |
  Rename-Item -Newname {"PPPPPPP_{0:d3}.jpg" -f $global:nr++}

where PPPPPPP is the name of parent folder containing these files.

Expected Output :

PPPPPPP_001.jpg
PPPPPPP_002.jpg
PPPPPPP_003.jpg

Files are located in C:\USER\MAIN\BLABLABLA\PPPPPPP folder.

Lightman answered 20/11, 2018 at 12:26 Comment(0)
C
1
  • Get the parent directory's name via $_.Directory.Name inside the script block.

  • Use Get-Variable to obtain a reference to the $nr sequence-number variable in the caller's scope, so you can modify its value directly (via .Value), which is preferable to using scope modifier $global: (-Scope 1 could be added to explicitly target the parent scope, but it isn't strictly necessary and omitted for brevity):

$nr = 1
Get-ChildItem -Filter *.jpg | Rename-Item -Newname {
  '{0}_{1:d3}.jpg' -f $_.Directory.Name, (Get-Variable nr).Value++
} -WhatIf

-WhatIf previews the renaming operation; remove it, once you're confident that the command will perform as intended.

  • A pragmatic shortcut, if your command is running in the top-level scope of a script file, is to use the $script: scope modifier to refer to a variable in that scope:
$nr = 1
Get-ChildItem -Filter *.jpg | Rename-Item -Newname {
  '{0}_{1:d3}.jpg' -f $_.Directory.Name, $script:nr++
} -WhatIf
  • A scope-agnostic alternative that is similarly concise - but more obscure - is to cast the $nr variable to [ref] so that you can modify its value directly in the caller's scope (via .Value).
$nr = 1
Get-ChildItem -Filter *.jpg | Rename-Item -Newname {
  '{0}_{1:d3}.jpg' -f $_.Directory.Name, ([ref] $nr).Value++
} -WhatIf
  • Finally, another alternative is to use an aux. hashtable
$nr = @{ Value = 1 }
Get-ChildItem -Filter *.jpg | Rename-Item -Newname {
  '{0}_{1:d3}.jpg' -f $_.Directory.Name, $nr.Value++
} -WhatIf

The following section explains these techniques.


Optional reading: Modifying the caller's variables in a delay-bind script block or calculated property:

The reason you couldn't just use $nr++ in your script block in order to increment the sequence number directly is:

  • Delay-bind script blocks (such as the one passed to Rename-Item -NewName) and script blocks in calculated properties run in a child scope.

  • Therefore, attempting to modify the caller's variables instead creates a block-local variable that goes out of scope in every iteration, so that the next iteration again sees the original value:

  • As an aside: A proposed future enhancement would obviate the need to maintain sequence numbers manually, via the introduction of an automatic $PSIndex variable that reflects the sequence number of the current pipeline object: see GitHub issue #13772.

Using a calculated property as an example:

PS> $nr = 0; 1..2 | Select-Object { '#' + ++$nr }

 '#' + ++$nr 
-------------
#1
#1   # !! the *caller's* $nr was NOT incremented 

While you can use a scope modifier such as $global: or $script: to explicitly reference a variable in a parent scope, these are absolute scope references that may not work as intended: Case in point: if you move your code into a script, $global:nr no longer refers to the variable created with $nr = 1.

Quick aside: Creating global variables should generally be avoided, given that they linger in the current session, even after a script exits.

The robust approach is to use a Get-Variable -Scope 1 call to robustly refer to the immediate parent scope:

PS> $nr = 0; 1..2 | Select-Object { '#' + ++(Get-Variable -Scope 1 nr).Value }

 '#' + ++(Get-Variable -Scope 1 nr).Value
------------------------------------------
#1
#2  # OK - $nr in the caller's scope was incremented

While this technique is robust, the cmdlet call introduces overhead (though that likely won't matter in practice), and it is a bit verbose, but:

  • you may omit the -Scope argument for brevity.

  • alternatively, you can improve the efficiency as follows:

    $nr = 0; $nrVar = Get-Variable nr
    1..2 | Select-Object { '#' + ++$nrVar.Value }
    

You can use the $script: scope modifier to refer to a variable in the top-level scope of a script file, which is a pragmatic shortcut for commands that run directly in that scope:

$nr = 0; 1..2 | Select-Object { '#' + ++$script:nr }

Using the [ref] type offers a more concise scope-agnostic alternative, though the solution is a bit obscure:

$nr = 0; 1..2 | Select-Object { '#' + ++([ref] $nr).Value }

Casting a variable to [ref] returns an object whose .Value property can access - and modify - that variable's value. Note that since $nr isn't being assigned to at that point, it is indeed the caller's $nr variable that is referenced.

If you don't mind using an aux. hashtable, you can take advantage of the fact that a hashtable is a .NET reference type, which means that the child scope in which the delay-bind script block runs sees the very same object as the caller's scope, and modifying a property (entry) of this object therefore persists across calls:

$nr = @{ Value = 0 }; 1..2 | Select-Object { '#' + ++$nr.Value }
Christine answered 20/11, 2018 at 13:23 Comment(0)
Q
0

EDIT: modified due to mklement0s hint.
To get the parent name, use .Directory.Name as another parameter for the format operator

$nr=1
Get-ChildItem *.jpg -file|
  Rename-Item -Newname {"{0}_{1:d3}.jpg" -f $_.Directory.Name,([ref]$nr).Value++} -whatIf

If the output looks OK remove the -WhatIf

This will only work while there are no overlapping ranges doing the rename, in that case you should probaply use a temporary extension.

Sample output on my German locale Windows

WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\150.jpg Ziel: A:\test\test_007.jpg".
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\151.jpg Ziel: A:\test\test_008.jpg".
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\152.jpg Ziel: A:\test\test_009.jpg".
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\153.jpg Ziel: A:\test\test_010.jpg".
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\154.jpg Ziel: A:\test\test_011.jpg".
WhatIf: Ausführen des Vorgangs "Datei umbenennen" für das Ziel "Element: A:\test\155.jpg Ziel: A:\test\test_012.jpg".
Quadrinomial answered 20/11, 2018 at 13:5 Comment(0)
G
0

Another, slightly different way:

$nr=1;Get-ChildItem -Filter *.jpg |
Rename-Item -Newname {"$(split-path -leaf $_.Directory)_{0:d3}.jpg" -f
$global:nr++}
Glazier answered 30/6, 2019 at 22:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.