sergey vasin

The IT blog

Remove-sthPreviousModuleVersions: функция для удаления предыдущих версий модулей

leave a comment »

Модуль 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


Реклама

Written by Сергей Васин

Июль 27, 2017 в 16:54

Опубликовано в PowerShell

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s