Модуль PowerShellGet — очень удобная вещь. Мы можем легко находить, устанавливать и обновлять модули из PowerShell Gallery и других зарегистрированных репозиториев при помощи командлетов Find-Module, Install-Module и Update-Module.
Однако, вследствие того факта, что PowerShell 5.0 и выше теперь поддерживает возможность присутствия в системе нескольких версий модулей одновременно (что само по себе является полезной возможностью), то при обновлении этих модулей с использованием командлета Update-Module, предыдущие их версии остаются в системе.
Если вы не обнаружили каких-либо ошибок в новой версии модуля, его предыдущую версию можно удалить. Если это один или пара модулей, эта задача не отличается какой-то особой сложностью. Но если обновленных модулей с десяток или больше, то работы по ручному удалению всех их предыдущих версий могут несколько затянуться.
Давайте напишем функцию, которая будет удалять предыдущие версии модулей, оставшиеся в системе после их обновления.
Function
Начнем с определения имени функции. Назовем ее Remove-sthPreviousModuleVersions.
function Remove-sthPreviousModuleVersions { }
CmdletBinding
Забегая вперед, решим, что, так как вопрос касается удаления модулей, неплохо было бы использовать функциональность, реализуемую параметрами -WhatIf и -Confirm. Для этого зададим атрибут CmdletBinding с аргументом SupportsShouldProcess, равняющимся $true. О том, для чего конкретно он нужен, поговорим чуть позже.
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] }
Param
Перейдем к параметрам.
Давайте сделаем так, что без указания параметров, функция удаляла бы предыдущие версии для всех модулей, у которых они есть. В случае, если нам потребуется удалить предыдущие версии только для определенных модулей, мы сможем указать их в качестве значения параметра -ModuleName.
Таким образом, решим, что значения параметра будут строками, и можно будет указать как одно имя модуля, так и несколько. Для этого перед именем параметра мы укажем тип [string[]]. Вторая, внутренняя, пара квадратных скобок указывает на то, что значением параметра может быть массив.
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] Param( [string[]]$ModuleName ) }
Get-InstalledModule
Далее нам нужно получить все модули, которые были установлены при помощи комадлетов модуля PowerShellGet, и, соответственно, поддерживают обновление посредством командлета Update-Module.
Для этого, вместо первым приходящего на ум командлета Get-Module, мы воспользуемся входящим в модуль PowerShellGet командлетом Get-InstalledModule, задача которого состоит именно в получении информации обо всех обновляемых модулях.
Без указания параметра -Name, в качестве результата командлет Get-InstalledModule выводит все модули, установленные при помощи командлетов Install-Module или Update-Module. При использовании параметра -Name выводится информация только об указанном модуле.
Для того, чтобы не проверять, использовался ли параметр -ModuleName при вызове функции и на основе этого решать, какую форму командлета Get-InstalledModule использовать, с параметром -Name или без, в качестве значения по умолчанию для параметра -ModuleName зададим «*».
Это даст нам возможность использования командлета Get-InstalledModule с параметром -Name вне зависимости от действий пользователя с тем же результатом, как если бы мы использовали две разные формы вызова командлета.
Полученную информацию сохраним в переменной $Modules.
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] Param( [string[]]$ModuleName = "*" ) $Modules = Get-InstalledModule -Name $ModuleName }
ForEach
Так как мы решили, что в качестве значения параметра -ModuleName наша функция будет принимать как один, так и несколько имен модулей, воспользуемся функциональностью конструкции ForEach. Потребуется нам это для того, чтобы обработать каждый модуль из переменной $Modules по-отдельности.
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] Param( [string[]]$ModuleName = "*" ) $Modules = Get-InstalledModule -Name $ModuleName foreach ($m in $Modules) { } }
AllVersions
Возвращаясь к командлету Get-InstalledModule, заметим, что использование параметра -Name позволяет указать еще один параметр -AllVersions, что в свою очередь дает нам возможность получить информацию о каждой версии указанного модуля, а не только о самой последней, что происходит без указания -AllVersions.
Когда мы ранее использовали этот командлет, нам нужна была информация о модулях, поддерживающих обновление, безотносительно количества их версий. Теперь же нам потребуется информация обо всех установленных версиях каждого обрабатываемого модуля.
Для этого, внутри блока ForEach мы используем командлет Get-InstalledModule с указанием обоих параметров, -Name и -AllVersions, и сохраним полученный результат в переменной $AllModuleVersions.
Кроме того, при вызове команды мы используем конструкцию @(), что позволяет нам представить ее результаты в виде массива, даже в том случае, если в результате выполнения командлета Get-InstalledModule был получен только один объект. Делаем мы это для того, чтобы можно было воспользоваться свойством count, что понадобится нам чуть дальше.
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] Param( [string[]]$ModuleName = "*" ) $Modules = Get-InstalledModule -Name $ModuleName foreach ($m in $Modules) { $AllModuleVersions = @(Get-InstalledModule -Name $m.Name -AllVersions) } }
$AllModuleVersions.count
Теперь нам нужно определить, какие модули представлены более чем одной версией, чтобы затем решить, какие их версии подлежат удалению. Для этого мы исползуем конструкцию if, где в качестве условия задаем, что свойство count массива объектов, содержащихся в переменной $AllModuleVersions, должно быть больше единицы.
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] Param( [string[]]$ModuleName = "*" ) $Modules = Get-InstalledModule -Name $ModuleName foreach ($m in $Modules) { $AllModuleVersions = @(Get-InstalledModule -Name $m.Name -AllVersions) if ($AllModuleVersions.Count -gt 1) { } } }
Sort и Select
Далее нам нужно подготовить полученные данные к дальнейшей обработке. Для начала мы отсортируем полученный массив по свойству Version в убывающем порядке. Результат этой сортировки мы сохраним в переменной $AllModuleVersionsSorted.
Кроме того, мы создадим вторую переменную, которая будет содержать только подлежащие удалению версии модулей. Так как переменная $AllModuleVersionsSorted уже соджерит в себе все модули, расположенные по убыванию версий, нам нужно только получить все версии, за исключением первой.
Для этого мы воспользуемся командлетом Select-Object с параметром -Skip, а результаты его выполнения сохраним в переменную $toUninstall.
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] Param( [string[]]$ModuleName = "*" ) $Modules = Get-InstalledModule -Name $ModuleName foreach ($m in $Modules) { $AllModuleVersions = @(Get-InstalledModule -Name $m.Name -AllVersions) if ($AllModuleVersions.Count -gt 1) { $AllModuleVersionsSorted = $AllModuleVersions | Sort-Object -Property Version -Descending $toUninstall = $AllModuleVersionsSorted | Select-Object -Skip 1 } } }
Output
Теперь давайте подумаем о выводе. Было бы неплохо, чтоб пользователь нашей функции имел полную картину относительно того, что в какой момент времени происходит.
Кроме того, ранее мы решили, что функция будет использовать функциональность, представленную параметрами -WhatIf и -Confirm. Поэтому хотелось бы, чтобы выводимая информация как при непосредственном удалении, так и при использовании параметров -WhatIf или -Confirm отличалась в минимальной степени. Нужно это для того, чтобы пользователю не приходилось разбираться в трех разных форматах вывода, сообщающих по сути об одном и том же.
С другой стороны, было бы неплохо, если бы реализовано это было с использованием минимального объема кода. Но здесь есть один момент.
Параметры -WhatIf и -Confirm, о чем мы поговорим чуть позже, будут срабатывать еще до процесса удаления модулей, но они уже должны будут сообщать пользователю, какие версии обрабатываемого в данный момент модуля будут удалены.
С другой стороны, сообщения, информируюящие пользователя о том, что в данный момент удаляется, должны быть расположены как можно ближе к коду, выполняющему это удаление.
К примеру, мы можем сообщить пользователю, что будут удалены версии 0.1, 0.2 и 0.3. И после этого начать, собственно, само удаление. Только в этом случае пользователю будет неизвестно, что происходит и какая версия модуля в данный момент удаляется.
Если же версий, подлежащих удалению больше одной, процесс удаления может занять некоторое ощутимое, с точки зрения привыкшего к интерактивной работе пользователя, время.
Поэтому более походящим вариантом будет вывод предназначенной для удаления версии модуля непосредственно перед самим ее удалением. Таким образом пользователь будет точно знать, чем занимается функция в каждый определенный момент.
Теперь давайте определимся со структурой данных. Пусть они будут представлены в следующем виде:
Module: ModuleName Latest version: 3.1.0 Removing version: 3.0.1 Removing version: 3.0.0
Таким образом, при использовании параметров -WhatIf или -Confirm для эта структура будет выводиться сразу, а при обычной работе функции, то есть при удалении предыдущих версий модулей, сначала будут выведены первые две строки, а те что сообщают о версиях, подлежащих удалению, непосредственно перед их, версий, удалением.
То есть, получается что первые две строки мы можем подготовить к выводу заранее и использвать их в любом из вариантов использования функции. А строки, касающиеся удаления, добавим по ходу дела.
Для форматирования данных мы используем оператор -f. Таким образом, строку для вывода со специальными символами в виде {0} или {1} мы указываем слева от него. А значения, которые и будут расположены вместо цифр в фигурных скобках — справа.
При указании значений мы используем так называемые подвыражения (subexpression). Выглядят они как некая строка, заключенная в скобки с предшествующим символом $ и служат для того, чтобы находящийся в них код был выполнен первым, а уже результат его выполнения рассматривался в качестве аргумента оператора -f.
Получившуюся в итоге заготовку мы сохраним в переменной $out.
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] Param( [string[]]$ModuleName = "*" ) $Modules = Get-InstalledModule -Name $ModuleName foreach ($m in $Modules) { $AllModuleVersions = @(Get-InstalledModule -Name $m.Name -AllVersions) if ($AllModuleVersions.Count -gt 1) { $AllModuleVersionsSorted = $AllModuleVersions | Sort-Object -Property Version -Descending $toUninstall = $AllModuleVersionsSorted | Select-Object -Skip 1 $out = "`nModule: {0}`nLatest version: {1}" -f $($AllModuleVersionsSorted[0].Name), $($AllModuleVersionsSorted[0].Version) } } }
ShouldProcess
Теперь давайте перейдем к реализации функциональности -WhatIf и -Confirm. Для этого мы воспользуемся методом ShouldProcess расположенного в переменной $PSCmdlet объекта System.Management.Automation.PSScriptCmdlet.
Метод ShouldProcess позволяет указать от одного до четырех аргументов. Нам же понадобится вариант с тремя параметрами, который позволяет явным образом указать формат вывода информации при его срабатывании.
В этом случае, первый параметр указывает текст, отображающийся при использовании параметра -WhatIf, а второй и третий определяют выводимую информацию при использовании параметра -Confirm.
Так как мы решили, что вывод во всех трех случаях должен быть максимально похож, мы укажем только первый аргумент метода ShouldProcess, а два оставшихся зададим в виде пустых строк. Это приведет к тому, что и -WhatIf и -Confirm будут отображать заданный в первом аргументе текст, в том виде, в котором он указан.
Сам метод ShouldProcess работает следующим образом. При использовании параметра -WhatIf, он выводит заданный текст и возвращает $false. Таким образом, указание его в качестве условия конструкции if приводит к тому, что при использовании параметра -WhatIf код, указанный в следующих за выражением if фигурных скобках, выполнен не будет.
При указании параметра -Confirm результат выполнения метода ShouldProcess зависит от действий пользователя. Если пользователь в качестве ответа на запрос указывает «Yes» или «Yes to All», результатом будет $true, и, соответственно, код выполнится. Если же пользователь выбирает «No» или «No to All», метод возвращает $false.
Причем, выбор вариантов «Yes to All» или «No to All» влияет на все последующие срабатывания метода ShouldProcess. В этом случае в качестве указанного пользователем варианта будет использоваться выбранное ранее значение.
Возвращаемся к аргументам метода ShouldProcess. В качестве первого из них мы зададим созданную нами ранее переменную $out, где уже содержатся первые две строки вывода.
Вслед за ней мы укажем уже знакомую нам конструкцию — подвыражение (subexpression), внутри которой мы получим номера версий всех содержащихся в переменной $toUninstall модулей и для каждого номера версии простроим строку в виде «Removing Version: номер_версии».
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] Param( [string[]]$ModuleName = "*" ) $Modules = Get-InstalledModule -Name $ModuleName foreach ($m in $Modules) { $AllModuleVersions = @(Get-InstalledModule -Name $m.Name -AllVersions) if ($AllModuleVersions.Count -gt 1) { $AllModuleVersionsSorted = $AllModuleVersions | Sort-Object -Property Version -Descending $toUninstall = $AllModuleVersionsSorted | Select-Object -Skip 1 $out = "`nModule: {0}`nLatest version: {1}" -f $($AllModuleVersionsSorted[0].Name), $($AllModuleVersionsSorted[0].Version) if ($PSCmdlet.ShouldProcess("$out $($toUninstall.Version | ForEach-Object { Write-Output -InputObject "`nRemoving Version: $PSItem"})", "", "")) { } } } }
Uninstall-Module
Теперь перейдем непосредственно к процессу удаления модулей. Для начала нам нужно вывести информацию, содержащуюся в переменной $out, об обрабатываем модуле и его последней версии, той что не будет затронута удалением.
Далее мы воспользуемся конструкцией foreach для обработки всех модулей из переменной $toUninstall. Как мы помним, там находятся все версии, кроме последней.
Внутри блока forech мы выводим информацию о том, какая версия сейчас будет удалена, и, собственно, удаляем ее.
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] Param( [string[]]$ModuleName = "*" ) $Modules = Get-InstalledModule -Name $ModuleName foreach ($m in $Modules) { $AllModuleVersions = @(Get-InstalledModule -Name $m.Name -AllVersions) if ($AllModuleVersions.Count -gt 1) { $AllModuleVersionsSorted = $AllModuleVersions | Sort-Object -Property Version -Descending $toUninstall = $AllModuleVersionsSorted | Select-Object -Skip 1 $out = "`nModule: {0}`nLatest version: {1}" -f $($AllModuleVersionsSorted[0].Name), $($AllModuleVersionsSorted[0].Version) if ($PSCmdlet.ShouldProcess("$out $($toUninstall.Version | ForEach-Object { Write-Output -InputObject "`nRemoving Version: $PSItem"})", "", "")) { Write-Output -InputObject $out foreach ($u in $toUninstall) { Write-Output "Removing version: $($u.Version)" Uninstall-Module -InputObject $u } } } } }
Install-Module
Полный текст функции будет выглядеть следующим образом:
function Remove-sthPreviousModuleVersions { [CmdletBinding(SupportsShouldProcess = $true)] Param( [string[]]$ModuleName = "*" ) $Modules = Get-InstalledModule -Name $ModuleName foreach ($m in $Modules) { $AllModuleVersions = @(Get-InstalledModule -Name $m.Name -AllVersions) if ($AllModuleVersions.Count -gt 1) { $AllModuleVersionsSorted = $AllModuleVersions | Sort-Object -Property Version -Descending $toUninstall = $AllModuleVersionsSorted | Select-Object -Skip 1 $out = "`nModule: {0}`nLatest version: {1}" -f $($AllModuleVersionsSorted[0].Name), $($AllModuleVersionsSorted[0].Version) if ($PSCmdlet.ShouldProcess("$out $($toUninstall.Version | ForEach-Object { Write-Output -InputObject "`nRemoving Version: $PSItem"})", "", "")) { Write-Output -InputObject $out foreach ($u in $toUninstall) { Write-Output "Removing version: $($u.Version)" Uninstall-Module -InputObject $u } } } } }
Эта функция является частью модуля sthTools, который вы можете установить из PowerShell Gallery при помощи следующей команды:
Install-Module -Name sthTools
Все модули доступны по следующей ссылке:
https://sergeyvasin.net/modules/
Страницы в социальных сетях:
Twitter: https://twitter.com/vsseth
Facebook: https://fb.com/inpowershell
VKontakte: https://vk.com/inpowershell