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

99封情书 提交于 2019-12-12 08:58:13

问题


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?



回答1:


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 and ForEach-Object, which run directly in the caller's scope.
      It is unclear whether that difference in behavior is intentional.
  • 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.




回答2:


@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
    }



回答3:


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!