3

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

ΩmegaMan
  • 29,542
  • 12
  • 100
  • 122

3 Answers3

5

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
1

@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
    }
Bacon Bits
  • 30,782
  • 5
  • 59
  • 66
  • Yes, the `[ref]` solution is obscure, so I've decided to restructure my answer to offer the - pragmatic, but limited - `$script:count++` solution first. For a small number of renaming operations it may not matter, but the fact that your solution invokes `Rename-Item` _for each input file_ can become a performance concern. – mklement0 Jul 02 '19 at 01:52
  • @mklement0 If performance were a concern, then I'd use System.IO.File.Move() directly or else rewrite the script using Python. – Bacon Bits Jul 02 '19 at 03:05
  • Having to resort to "non-native" means is always awkward, and a barrier to many. With a large enough input set, using delay-bind script blocks offers a significant performance advantage over a `ForEach-Object` call with repeated invocations - and that is (a) worth pointing out and (b) may be good enough in a given situation. So, as is often the case, it's a matter of degrees of PowerShell performance and comes down to choosing the right constructs. Failing that, yes, you can look to direct use of the .NET framework or external executables. – mklement0 Jul 02 '19 at 04:12
  • @mklement0 "Having to resort to "non-native" means is always awkward, and a barrier to many." Says the person trying to explain delay-bind and references? Come on. – Bacon Bits Jul 02 '19 at 20:22
  • Delay-bind script blocks are an expressive, concise and - relatively - performant standard feature that deserves much wider use; it languished due to lack of documentation until fairly recently; I hope my answers help popularize it a bit. That its implementation is flawed with respect to scoping is unfortunate - if you agree, make your voice heard [here](https://github.com/PowerShell/PowerShell/issues/7157). My answer now features the - reasonable - `$script:count++` workaround first; those looking for a more robust solution can use the - undoubtedly obscure - `[ref]` solution. – mklement0 Jul 03 '19 at 03:29
1

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' }
js2010
  • 23,033
  • 6
  • 64
  • 66
  • A `-Begin` script block to initialize the `$count` variable is conceptually appealing, but no different than initializing `$count` _before_ the `foreach` (`ForEach-Object`) call, because `ForEach-Object` script blocks run directly in the caller's scope. In other words: the `$count` variable lingers in the caller's scope either way. It may not always matter, but calling `Rename-Item` _for each input object_ is inefficient compared to a single invocation with a delay-bind script block. A simpler way to avoid the full-path `$_` problem (no longer a problem in PS Core) is to use `$_.FullName` – mklement0 Jul 07 '19 at 21:54
  • @mklement0 This is actually 3 script blocks passed to -process, but they serve the same function. – js2010 Jul 07 '19 at 22:07
  • You're correct - they technically all bind to `-Process`, but are effectively treated as if they had been passed to `-Begin`, `-Process`, and `-End`, respectively. – mklement0 Jul 07 '19 at 22:20