Get-InstalledModule の結果をグループ化して表示する

はじめに

本記事は PowerShell Advent Calendar 2018 の 6 日目です。
PowerShell Advent Calendar 2018 は寄稿して頂ける方を絶賛募集中です。よろしくお願いいたします。
qiita.com

動機

PowerSheller であれば、Get-Module -ListAvailable って、数えきれないほど叩いてきたと思います。
このコマンドの出力は、こんな感じになりますね。

PS> Get-Module -ListAvailable

    ディレクトリ: C:\Users\aetos\Documents\WindowsPowerShell\Modules

ModuleType Version    Name                 ExportedCommands
---------- -------    ----                 ----------------
Script     0.7.0      Az.Aks               {Get-AzAks, New-AzAks, Remove-AzAks, Import-AzAksCredential...}
Script     0.7.0      Az.AnalysisServices  {Resume-AzAnalysisServicesServer, Suspend-AzAnalysisServicesServer, Get-AzAnalysisServicesServer, Rem...
Script     0.7.0      Az.ApiManagement     {Add-AzApiManagementRegion, Get-AzApiManagementSsoToken, New-AzApiManagementHostnameConfiguration, Ne...
...

    ディレクトリ: C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules

ModuleType Version    Name                 ExportedCommands
---------- -------    ----                 ----------------
Manifest   1.0.1.0    ActiveDirectory      {Add-ADCentralAccessPolicyMember, Add-ADComputerServiceAccount, Add-ADDomainControllerPasswordReplica...
Manifest   1.0.0.0    AppBackgroundTask    {Disable-AppBackgroundTaskDiagnosticLog, Enable-AppBackgroundTaskDiagnosticLog, Set-AppBackgroundTask...
Manifest   2.0.0.0    AppLocker            {Get-AppLockerFileInformation, Get-AppLockerPolicy, New-AppLockerPolicy, Set-AppLockerPolicy...}
...

一方、PowerShellGet で導入された類似コマンドである Get-InstalledModule の出力は、こんな感じです。

PS> Get-InstalledModule

Version  Name                    Repository  Description
-------  ----                    ----------  -----------
5.1.2    Azure                   PSGallery   Microsoft Azure PowerShell - Service Management
0.5.0    Azure.AnalysisServices  PSGallery   Microsoft Azure PowerShell - Analysis Services server management
4.2.1    Azure.Storage           PSGallery   Microsoft Azure PowerShell - Storage service cmdlets. Manages blobs, queues, tabl...
...

個人的には、この Get-InstalledModule の結果が、Get-Module のように、インストール ディレクトリでグループ化されないことが不満でした。
Get-Module は、別にいいんですよ。どこにインストールされていようが関係ないんです。インポートさえできれば。

でも、Get-InstalledModule は、インストール場所を気にすることが、一点だけあります。
それはモジュールの更新時。
ユーザー プロファイル ディレクトリ(C:\Users\aetos)以下にインストールされていれば、ユーザー権限で更新ができます。
グローバル インストール(C:\Program Files\WindowsPowerShell\Modules)だったら、管理者権限でないと更新ができません。

というわけで、(やや強引な動機ではありますが)Get-InstalledModule の結果が、インストール場所でグループ化されるようにしてみましょう。

ま、過去にこんなことがあったので、ちょっと気にしているという理由はあります。
tech.blog.aerie.jp

目標

実現したいイメージはこうです。

PS> Get-InstalledModule

    ディレクトリ: C:\Users\aetos\Documents\WindowsPowerShell\Modules

Version  Name                    Repository  Description
-------  ----                    ----------  -----------
5.1.2    Azure                   PSGallery   Microsoft Azure PowerShell - Service Management
0.5.0    Azure.AnalysisServices  PSGallery   Microsoft Azure PowerShell - Analysis Services server management
4.2.1    Azure.Storage           PSGallery   Microsoft Azure PowerShell - Storage service cmdlets. Manages blobs, queues, tabl...
...

言葉で書くと

  • 新しいコマンドは導入せず、Get-InstalledModule でできるようにする
  • 出力結果をインストール場所でグループ化する
  • 出力結果の互換性を保つ

の 3 つです。

「出力結果の互換性を保つ」というのは、出力されるオブジェクトのメンバーをいじらないということです。
もし Get-InstalledModule コマンドを内部で実行しているスクリプトがあっても、それが壊れないようにするためです。

完成品

こちらです。


gisteb9543af259e38346acde5f1440b8b74

この 2 つのファイルを同じディレクトリにおいて、PowerShell プロファイルで PowerShellGetUtility.ps1 を読み込むだけです。

説明

やっていることは単純です。

同名、同引数の Get-InstalledModule というコマンドを定義し、その中で本来の PowerShellGet\Get-InstalledModule コマンドを呼び出し、結果を InstalledLocation でソートして、独自の型名を付けて返しているだけですね。
グループ化は、Add-Member でつけた型名に対して、InstalledModule.format.ps1xml で設定しています。

アドホックにやるなら、Format-Table コマンドの -GroupBy オプションで可能です。
ただ、コマンド内で Format-Table したものを出力してしまうと、型の互換性が崩れてしまいます。
変えたいのは表示だけなので、format.ps1xml ファイルで行うのが適切です。

format.ps1xml でやるにせよ、Format-Table でやるにせよ、グループ化したい値でソートしておかないと正常に動きません。
もし、こうしたコマンド自体の追加処理が不要なのであれば、ps1xml だけでも可能です。
tech.blog.aerie.jp

モジュールの InstalledLocation プロパティは

C:\Users\aetos\Documents\WindowsPowerShell\Modules\Az\0.7.0

のようになっていますので、ここからモジュール名とバージョンを削るために Split-Path -Parent を 2 回呼んだものでグループ化しています。

追加したオプション -Raw は、何も手を加えない、オリジナルの Get-InstalledModule を実行するためのオプションです。
まぁ、

PS> PowerShellGet\Get-InstalledModule

って叩いても同じことなんですが、こっちのほうが楽なので。

出力オブジェクトには(型名を追加した以外は)手を加えていないので、今まで通り

PS> Get-InstalledModule | Update-Module

と打っても動きます。

SteppablePipeline でごにょごにょしているのは、パイプライン経由で呼ばれた場合に、

  1. このコマンドの begin
  2. PowerShellGet\Get-InstalledModule の begin
  3. このコマンドの process
  4. PowerShellGet\Get-InstalledModule の process
  5. このコマンドの end
  6. PowerShellGet\Get-InstalledModule の end

という順番で実行されるようにするためです。
まぁ今回の場合、(-Raw をつけなければ)Sort-Object しているので、主な処理は end に寄ってしまうのですが。

ヘルプについても、

.ForwardHelpTargetName PowerShellGet\Get-InstalledModule

と書くことで、

Get-Help Get-InstalledModule

と打った時に、オリジナルのヘルプが出るようにしてあります。

既存のコマンドと同名のコマンドを定義することで、既存のコマンドをオーバーライドしてしまうのが良いことなのかと思われるかもしれません。
が、これは PowerShell 的に OK なのです。
Out-Default コマンドの説明を見るとわかるのですが、これは、まさにそのようにオーバーライドされることを意図して用意されたコマンドです。
Out-Default コマンドが特別というよりは、一般的に OK なことであると解釈しています。

まとめ

短いながらも、いろいろなエッセンスが詰まったサンプルになっており、応用範囲はそれなりに広いと思われます。
機会があったら参考にしてみてください。