++ Оператор для переменной не меняется, как ожидалось в ScriptBlock

Я пытаюсь переименовать файлы, добавив префикс, основанный на увеличивающемся счетчике в файлах, например:

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

gci $directory | sort -Property LastWriteTime | `
rename-item -newname {"{0}_{1}" -f $count++, $_.Name} -whatif

Тем не менее, все обрабатываемые файлы 71_ и $count в $count++ никогда не увеличиваются, а имена файлов имеют одинаковый префикс? Почему?


введите описание изображения здесь


person ΩmegaMan    schedule 01.07.2019    source источник
comment
Уверен, что у вас проблемы с определением объема работ. Попробуйте изменить все ссылки с $count на $script:count.   -  person TheMadTechnician    schedule 02.07.2019


Ответы (3)


Причина, по которой вы не можете просто использовать $count++ в своем блоке скрипта для прямого увеличения порядкового номера:

  • Блоки сценария с отложенной привязкой - например, тот, который вы передали в Rename-Item -NewName - и блоки сценария в вычисляемые свойства выполняются в дочерней области .

  • Следовательно, попытка изменить переменные вызывающей стороны вместо этого создает block -local переменную, которая выходит за пределы области видимости на каждой итерации, так что на следующей итерации снова будет видно исходное значение из область вызывающего абонента.

    • To learn more about scopes and implicit local-variable creation, see this answer.

Обходные пути

Прагматичный, но потенциально ограничивающий обходной путь - использовать спецификатор области $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 была инициализирована в области верхнего уровня сценария.

    • That is, if you moved your code into a function with a function-local $count variable, it would no longer work.

Для гибкого решения требуется надежная относительная ссылка на родительскую область действия:

Есть два варианта:

  • концептуально ясно, но многословно и сравнительно медленно из-за необходимости вызова командлета: (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
  • несколько непонятно, но быстрее и лаконичнее: ([ref] $count).Value++
gci $directory | sort -Property LastWriteTime |
  rename-item -newname { '{0}_{1}' -f ([ref] $count).Value++, $_.Name } -whatif

[ref] $count фактически то же самое, что Get-Variable -Scope 1 count (при условии, что переменная $count была установлена ​​в родительской области)


Примечание. Теоретически вы можете использовать $global:count как для инициализации, так и для увеличения глобальной переменной в любой области, но, учитывая, что глобальные переменные сохраняются даже после завершения выполнения скрипта, вы должны тогда также заранее сохраните любое ранее существовавшее значение $global:count и восстановите его после, что делает этот подход непрактичным.

person mklement0    schedule 01.07.2019

@ mklement0 ответ правильный, но я думаю, что это намного легче понять, чем иметь дело со ссылками:

Get-ChildItem $directory | 
    Sort-Object -Property LastWriteTime |
    ForEach-Object {
        $NewName = "{0}_{1}" -f $count++, $_.Name
        Rename-Item $_ -NewName $NewName -WhatIf
    }
person Bacon Bits    schedule 01.07.2019
comment
Да, решение [ref] неясно, поэтому я решил реструктурировать свой ответ, чтобы сначала предложить - прагматичное, но ограниченное - решение $script:count++. Для небольшого числа операций переименования это может не иметь значения, но тот факт, что ваше решение вызывает Rename-Item для каждого входного файла, может стать проблемой для производительности. - person mklement0; 02.07.2019
comment
@ mklement0 Если бы производительность была проблемой, я бы использовал System.IO.File.Move () напрямую или переписал бы скрипт с помощью Python. - person Bacon Bits; 02.07.2019
comment
Приходиться прибегать к неродным средствам всегда неловко и является препятствием для многих. При достаточно большом входном наборе использование блоков сценария delay-bind дает значительное преимущество в производительности по сравнению с вызовом ForEach-Object с повторными вызовами - и это (а) стоит отметить и (б) может быть достаточно хорошим в данной ситуации. Итак, как это часто бывает, все зависит от степени производительности PowerShell и сводится к выбору правильных конструкций. В противном случае, да, вы можете обратиться к прямому использованию .NET framework или внешних исполняемых файлов. - person mklement0; 02.07.2019
comment
@ mklement0 Приходиться прибегать к неродным средствам всегда неудобно и является препятствием для многих. Говорит человек, пытающийся объяснить delay-bind и ссылки? Ну давай же. - person Bacon Bits; 02.07.2019
comment
Блоки сценария Delay-bind - выразительная, краткая и относительно производительная стандартная функция, которая заслуживает гораздо более широкого использования; до недавнего времени он томился из-за отсутствия документации; Надеюсь, мои ответы помогут немного популяризировать это. То, что его реализация ошибочна в отношении области видимости, прискорбно - если вы согласны, сделайте так, чтобы ваш голос был услышан здесь < / а>. В моем ответе сначала предлагается - разумный - $script:count++ обходной путь; те, кто ищет более надежное решение, могут использовать - несомненно непонятное - [ref] решение. - person mklement0; 03.07.2019

Вау, в последнее время это происходит очень часто. Вот моя любимая на данный момент альтернатива с несколькими блоками сценариев foreach. gci с подстановочным знаком дает полный путь к $ _ позже. Символ продолжения обратной кавычки после вертикальной черты или оператора не требуется.

$directory = 'c:\temp'

gci $directory\* | sort LastWriteTime |
foreach { $count = 71 } { rename-item $_ -newname ("{0}_{1}" -f
$count++, $_.Name) -whatif } { 'done' }
person js2010    schedule 02.07.2019
comment
Блок сценария -Begin для инициализации переменной $count концептуально привлекателен, но ничем не отличается от инициализации $count перед вызовом foreach (ForEach-Object), потому что блоки сценария ForEach-Object выполняются непосредственно в области действия вызывающего. Другими словами: переменная $count в любом случае остается в области действия вызывающего. Это может не всегда иметь значение, но вызов Rename-Item для каждого входного объекта неэффективен по сравнению с одним вызовом с блоком сценария привязки задержки. Более простой способ избежать проблемы $_ полного пути (больше не проблема в PS Core) - использовать $_.FullName - person mklement0; 08.07.2019
comment
@ mklement0 Это фактически 3 блока скрипта, переданных -process, но они выполняют одну и ту же функцию. - person js2010; 08.07.2019
comment
Вы правы - технически все они привязаны к -Process, но эффективно обрабатываются так, как если бы они были переданы в -Begin, -Process и -End соответственно. - person mklement0; 08.07.2019