I am trying to rename files by putting a prefix based on an incrementing counter in the files such as:
$directory = 'C:\Temp'
[int] $count=71;
gci $directory | sort -Property LastWriteTime | `
rename-item -newname {"{0}_{1}" -f $count++, $_.Name} -whatif
Yet all the files processed are 71_
and $count
in $count++
never increments and the filenames are prefixed the same? Why?
The reason you cannot just use $count++
in your script block in order to increment the sequence number directly is:
Delay-bind script blocks - such as the one you passed to
Rename-Item -NewName
- and script blocks in calculated properties run in a child scope.- Contrast this with script blocks passed to
Where-Object
andForEach-Object
, which run directly in the caller's scope.
It is unclear whether that difference in behavior is intentional.
- Contrast this with script blocks passed to
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 from the caller's scope.
- To learn more about scopes and implicit local-variable creation, see this answer.
Workarounds
A pragmatic, but potentially limiting workaround is to use scope specifier $script:
- i.e., $script:count
- to refer to the caller's $count
variable:
$directory = 'C:\Temp'
[int] $count=71
gci $directory | sort -Property LastWriteTime |
rename-item -newname { '{0}_{1}' -f $script:count++, $_.Name } -whatif
This will work:
in an interactive session (at the command prompt, in the global scope).
in a script, as long as the
$count
variable was initialized in the script's top-level scope.- That is, if you moved your code into a function with a function-local
$count
variable, it would no longer work.
- That is, if you moved your code into a function with a function-local
A flexible solution requires a reliable relative reference to the parent scope:
There are two choices:
- conceptually clear, but verbose and comparatively slow, due to having to call a cmdlet:
(Get-Variable -Scope 1 count).Value++
gci $directory | sort -Property LastWriteTime |
rename-item -newname { '{0}_{1}' -f (Get-Variable -Scope 1 count).Value++, $_.Name } -whatif
- somewhat obscure, but faster and more concise:
([ref] $count).Value++
gci $directory | sort -Property LastWriteTime |
rename-item -newname { '{0}_{1}' -f ([ref] $count).Value++, $_.Name } -whatif
[ref] $count
is effectively the same as Get-Variable -Scope 1 count
(assuming that a $count
variable was set in the parent scope)
Note: In theory, you could use $global:count
to both initialize and increment a global variable in any scope, but given that global variables linger even after script execution ends, you should then also save any preexisting $global:count
value beforehand, and restore it afterwards, which makes this approach impractical.
@mklement0's answer is correct, but I think this is much easier to understand than dealing with references:
Get-ChildItem $directory |
Sort-Object -Property LastWriteTime |
ForEach-Object {
$NewName = "{0}_{1}" -f $count++, $_.Name
Rename-Item $_ -NewName $NewName -WhatIf
}
Wow, this is coming up a lot lately. Here's my current favorite foreach multi scriptblock alternative. gci with a wildcard gives a full path to $_ later. You don't need the backtick continuation character after a pipe or an operator.
$directory = 'c:\temp'
gci $directory\* | sort LastWriteTime |
foreach { $count = 71 } { rename-item $_ -newname ("{0}_{1}" -f
$count++, $_.Name) -whatif } { 'done' }
来源:https://stackoverflow.com/questions/56843044/operator-on-variable-is-not-changing-as-expected-in-scriptblock