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 }