Search code examples
powershellscopepipescriptblockrename-item-cmdlet

++ Operator on Variable Is Not Changing As Expected In ScriptBlock


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?


enter image description here


Solution

  • 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.

    • 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.

    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.