Powershell ++;变量上的运算符未按ScriptBlock中的预期更改

Powershell ++;变量上的运算符未按ScriptBlock中的预期更改,powershell,scope,pipe,scriptblock,rename-item-cmdlet,Powershell,Scope,Pipe,Scriptblock,Rename Item Cmdlet,我试图通过在文件中添加基于递增计数器的前缀来重命名文件,例如: $directory = 'C:\Temp' [int] $count=71; gci $directory | sort -Property LastWriteTime | ` rename-item -newname {"{0}_{1}" -f $count++, $_.Name} -whatif 然而,所有处理的文件在$count++中都是71.和$count的,从不递增,并且文件名的前缀是相同的?为什么? 不能在脚本

我试图通过在文件中添加基于递增计数器的前缀来重命名文件,例如:

$directory = 'C:\Temp'
[int] $count=71; 

gci $directory | sort -Property LastWriteTime | `
rename-item -newname {"{0}_{1}" -f $count++, $_.Name} -whatif
然而,所有处理的文件在
$count++
中都是
71.
$count
的,从不递增,并且文件名的前缀是相同的?为什么?



不能在脚本块中使用
$count++
直接递增序列号的原因是:

  • -例如传递给
    重命名项-NewName
    -的脚本块,以及在子作用域中运行的脚本块

    • 将此与传递给
      Where Object
      ForEach Object
      的脚本块进行对比,它们直接在调用方的作用域中运行。
      是的
  • 因此,尝试修改调用者的变量会在每次迭代中创建一个超出范围的块局部变量,以便下一次迭代再次看到调用者范围中的原始值

    • 要了解有关作用域和隐式局部变量创建的更多信息,请参阅

权变措施 一个实用但可能有限制的解决办法是使用范围说明符
$script:
-即
$script:count
-引用调用方的
$count
变量:

$directory = 'C:\Temp'
[int] $count=71

gci $directory | sort -Property LastWriteTime |
  rename-item -newname { '{0}_{1}' -f $script:count++, $_.Name } -whatif
这将有助于:

  • 在交互式会话中(在命令提示下,在全局范围内)

  • 在脚本中,只要在脚本的顶级作用域中初始化了
    $count
    变量

    • 也就是说,如果您将代码移动到一个带有函数local
      $count
      变量的函数中,它将不再工作

灵活的解决方案需要对父范围进行可靠的相对引用

有两种选择:

  • 概念清晰,但冗长且相对较慢,因为必须调用cmdlet:
    (Get Variable-Scope 1 count)。Value++
  • 有些模糊,但更快更简洁。
    ([ref]$count)。Value++
[ref]$count
实际上与
获取变量-作用域1计数
相同(假设父作用域中设置了
$count
变量)



注意:理论上,您可以使用
$global:count
在任何范围内初始化和递增全局变量,但如果全局变量即使在脚本执行结束后仍然存在,那么您还应该事先保存任何先前存在的
$global:count
值,然后再恢复它,这使得这种方法不切实际。

@mklement0的答案是正确的,但我认为这比处理引用更容易理解:

Get-ChildItem $directory | 
    Sort-Object -Property LastWriteTime |
    ForEach-Object {
        $NewName = "{0}_{1}" -f $count++, $_.Name
        Rename-Item $_ -NewName $NewName -WhatIf
    }

哇,这件事最近经常发生。下面是我当前最喜欢的foreach多脚本块替代方案。带有通配符的gci提供了到$\的完整路径。管道或运算符后不需要反勾号连续字符

$directory = 'c:\temp'

gci $directory\* | sort LastWriteTime |
foreach { $count = 71 } { rename-item $_ -newname ("{0}_{1}" -f
$count++, $_.Name) -whatif } { 'done' }

很确定你遇到了范围问题。尝试将所有引用从
$count
更改为
$script:count
。是的,
[ref]
解决方案是模糊的,因此我决定重新构造我的答案,首先提供实用但有限的
$script:count++
解决方案。对于少量重命名操作,这可能无关紧要,但您的解决方案为每个输入文件调用
重命名项
这一事实可能会成为性能问题。@mklement0如果性能是一个问题,那么我会直接使用System.IO.file.Move(),或者使用Python重写脚本。必须求助于“非本机”手段总是笨拙的,对许多人来说是一个障碍。对于足够大的输入集,使用延迟绑定脚本块比重复调用的
ForEach对象
调用提供了显著的性能优势,这(a)值得指出,(b)在给定情况下可能足够好。因此,通常情况下,这取决于PowerShell性能的程度,最终取决于选择正确的结构。如果做不到这一点,是的,您可以直接使用.NET framework或外部可执行文件。@mklement0“不得不求助于“非本机”手段总是很尴尬,对许多人来说也是一个障碍。”试图解释延迟绑定和引用的人说?Delay-bind脚本块是一种表现力强、简洁且性能相对较好的标准功能,值得更广泛地使用;直到最近,由于缺乏文件,它才开始衰落;我希望我的答案有助于普及一点。不幸的是,它的实现在范围界定方面存在缺陷——如果您同意,请发表您的意见。我的答案现在的特点是-reasonal-
$script:count++
先解决问题;那些寻找更健壮的解决方案的人可以使用-无疑是模糊的-
[ref]
解决方案。一个
-Begin
脚本块来初始化
$count
变量在概念上很有吸引力,但与在
foreach
foreach Object
)调用之前初始化
$count
没有什么不同,因为
ForEach对象
脚本块直接在调用方的作用域中运行。换句话说,
$count
变量在调用方的作用域中以任何方式逗留。这可能并不总是很重要,但与使用延迟绑定脚本块的单个调用相比,为每个输入对象调用
重命名项
效率低下。避免完整路径问题(不再是PS Core中的问题)的一种更简单的方法是使用
$\uName
@mklement0这实际上是传递给-process的3个脚本块,但它们具有相同的功能。没错,它们在技术上都绑定到
-process
,但被有效地视为已传递给
-Begin
-Process
-End
、res
Get-ChildItem $directory | 
    Sort-Object -Property LastWriteTime |
    ForEach-Object {
        $NewName = "{0}_{1}" -f $count++, $_.Name
        Rename-Item $_ -NewName $NewName -WhatIf
    }
$directory = 'c:\temp'

gci $directory\* | sort LastWriteTime |
foreach { $count = 71 } { rename-item $_ -newname ("{0}_{1}" -f
$count++, $_.Name) -whatif } { 'done' }