PowerShell でオブジェクトの表示方法をカスタマイズする

Test-Connection という、対象のサーバーが生きてるかどうか Ping を投げてチェックするコマンドがあるわけですが、どうもこれが遅いらしいと。
原因と対策は、こちらのブログを見て頂くとして。

stknohg.hatenablog.jp

その対策に、週末にコメントすると言っといて、まだしていなかったので、慌てて書いているわけです。はい。

どうして遅いのか

Test-Connection が返す Win32_PingStatus オブジェクトに対して、PowerShell が表示するときに余計なメンバーを付加していて、その情報の取得に時間がかかっているんだ、というわけです。
この余計なメンバーはどうやって付加されているかと言うと、PowerShell における型定義ファイルである types.ps1xml というファイル*1に、こう書かれていることによります。

<Type>
  <Name>System.Management.ManagementObject#root\cimv2\Win32_PingStatus</Name>
  <Members>
    <ScriptProperty>
      <Name>IPV4Address</Name>
      <GetScriptBlock>
        $iphost = [System.Net.Dns]::GetHostEntry($this.address)
        $iphost.AddressList | ?{ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork } | select -first 1
      </GetScriptBlock>
    </ScriptProperty>
    <ScriptProperty>
      <Name>IPV6Address</Name>
      <GetScriptBlock>
        $iphost = [System.Net.Dns]::GetHostEntry($this.address)
        $iphost.AddressList | ?{ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6 } | select -first 1
      </GetScriptBlock>
    </ScriptProperty>
  </Members>
</Type>
...
<Type>
  <Name>Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Win32_PingStatus</Name>
  <Members>
    <ScriptProperty>
      <Name>IPV4Address</Name>
      <GetScriptBlock>
        $iphost = [System.Net.Dns]::GetHostEntry($this.address)
        $iphost.AddressList | ?{ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork } | select -first 1
      </GetScriptBlock>
    </ScriptProperty>
    <ScriptProperty>
      <Name>IPV6Address</Name>
      <GetScriptBlock>
        $iphost = [System.Net.Dns]::GetHostEntry($this.address)
        $iphost.AddressList | ?{ $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6 } | select -first 1
      </GetScriptBlock>
    </ScriptProperty>
  </Members>
</Type>

この余計なメンバーを削除してしまえばよいので、先の記事では、Remove-TypeData コマンドを使って、この types.ps1xml の内容を無効化してしまうというアプローチがとられています。

ただ、それってちょっと乱暴じゃない? と思うわけです。
Remove-TypeData で消してしまうと、プロパティそのものが無かったことになってしまいます。
表示するときに DNS クエリを行っているのが遅いので、表示だけ抑止してやれば良いのではないでしょうか。
場合によっては、Test-Connection コマンドがこれらのメンバーを返すことに依存しているスクリプトがあるかもしれませんし。

表示だけを無かったことにするには

PowerShell で Test-Connection と打つと、こんな表示になります。

PS> Test-Connection localhost

Source        Destination     IPV4Address      IPV6Address                              Bytes    Time(ms)
------        -----------     -----------      -----------                              -----    --------
xxxxxxxxx     localhost       127.0.0.1        ::1                                      32       0
xxxxxxxxx     localhost       127.0.0.1        ::1                                      32       0
xxxxxxxxx     localhost       127.0.0.1        ::1                                      32       0
xxxxxxxxx     localhost       127.0.0.1        ::1                                      32       0

テーブルみたいに表示されていますよね。
これは暗黙的に Format-Table コマンドによって書式設定されているからです。
明示的に書いてやっても同じ結果になります。

PS> Test-Connection localhost | Format-Table

Source        Destination     IPV4Address      IPV6Address                              Bytes    Time(ms)
------        -----------     -----------      -----------                              -----    --------
xxxxxxxxx     localhost       127.0.0.1        ::1                                      32       0
xxxxxxxxx     localhost       127.0.0.1        ::1                                      32       0
xxxxxxxxx     localhost       127.0.0.1        ::1                                      32       0
xxxxxxxxx     localhost       127.0.0.1        ::1                                      32       0

PowerShell でのオブジェクトの書式設定は、format.ps1xml というファイルに定義されています。
Win32_PingStatus オブジェクトであれば、DotNetTypes.format.ps1xml というファイル*2に書かれています。

<View>
  <Name>System.Management.ManagementObject#root\cimv2\Win32_PingStatus</Name>
  <ViewSelectedBy>
    <TypeName>System.Management.ManagementObject#root\cimv2\Win32_PingStatus</TypeName>
  </ViewSelectedBy>
  <TableControl>
    <TableHeaders>
      <TableColumnHeader>
        <Label>Source</Label>
        <Width>13</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>Destination</Label>
        <Width>15</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>IPV4Address</Label>
        <Width>16</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>IPV6Address</Label>
        <Width>40</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>Bytes</Label>
        <Width>8</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>Time(ms)</Label>
        <Width>9</Width>
      </TableColumnHeader>
    </TableHeaders>
    <TableRowEntries>
      <TableRowEntry>
        <TableColumnItems>
          <TableColumnItem>
            <PropertyName>__Server</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>Address</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>IPV4Address</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>IPV6Address</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>BufferSize</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>ResponseTime</PropertyName>
          </TableColumnItem>
        </TableColumnItems>
      </TableRowEntry>
    </TableRowEntries>
  </TableControl>
</View>
...
<View>
  <Name>Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Win32_PingStatus</Name>
  <ViewSelectedBy>
    <TypeName>Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Win32_PingStatus</TypeName>
  </ViewSelectedBy>
  <TableControl>
    <TableHeaders>
      <TableColumnHeader>
        <Label>Source</Label>
        <Width>13</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>Destination</Label>
        <Width>15</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>IPV4Address</Label>
        <Width>16</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>IPV6Address</Label>
        <Width>40</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>Bytes</Label>
        <Width>8</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>Time(ms)</Label>
        <Width>9</Width>
      </TableColumnHeader>
    </TableHeaders>
    <TableRowEntries>
      <TableRowEntry>
        <TableColumnItems>
          <TableColumnItem>
            <PropertyName>ComputerName</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>Address</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>IPV4Address</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>IPV6Address</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>BufferSize</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>ResponseTime</PropertyName>
          </TableColumnItem>
        </TableColumnItems>
      </TableRowEntry>
    </TableRowEntries>
  </TableControl>
</View>

なんとなく意味が分かると思います。分かれ。

ここから、IPV4Address と IPV6Address を削ってしまえばよいわけです。
こんな感じ。

<View>
  <Name>System.Management.ManagementObject#root\cimv2\Win32_PingStatus</Name>
  <ViewSelectedBy>
    <TypeName>System.Management.ManagementObject#root\cimv2\Win32_PingStatus</TypeName>
  </ViewSelectedBy>
  <TableControl>
    <TableHeaders>
      <TableColumnHeader>
        <Label>Source</Label>
        <Width>13</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>Destination</Label>
        <Width>15</Width>
      </TableColumnHeader>
<!--
      <TableColumnHeader>
        <Label>IPV4Address</Label>
        <Width>16</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>IPV6Address</Label>
        <Width>40</Width>
      </TableColumnHeader>
-->
      <TableColumnHeader>
        <Label>Bytes</Label>
        <Width>8</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>Time(ms)</Label>
        <Width>9</Width>
      </TableColumnHeader>
    </TableHeaders>
    <TableRowEntries>
      <TableRowEntry>
        <TableColumnItems>
          <TableColumnItem>
            <PropertyName>__Server</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>Address</PropertyName>
          </TableColumnItem>
<!--
          <TableColumnItem>
            <PropertyName>IPV4Address</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>IPV6Address</PropertyName>
          </TableColumnItem>
-->
          <TableColumnItem>
            <PropertyName>BufferSize</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>ResponseTime</PropertyName>
          </TableColumnItem>
        </TableColumnItems>
      </TableRowEntry>
    </TableRowEntries>
  </TableControl>
</View>
...
<View>
  <Name>Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Win32_PingStatus</Name>
  <ViewSelectedBy>
    <TypeName>Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Win32_PingStatus</TypeName>
  </ViewSelectedBy>
  <TableControl>
    <TableHeaders>
      <TableColumnHeader>
        <Label>Source</Label>
        <Width>13</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>Destination</Label>
        <Width>15</Width>
      </TableColumnHeader>
<!--
      <TableColumnHeader>
        <Label>IPV4Address</Label>
        <Width>16</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>IPV6Address</Label>
        <Width>40</Width>
      </TableColumnHeader>
-->
      <TableColumnHeader>
        <Label>Bytes</Label>
        <Width>8</Width>
      </TableColumnHeader>
      <TableColumnHeader>
        <Label>Time(ms)</Label>
        <Width>9</Width>
      </TableColumnHeader>
    </TableHeaders>
    <TableRowEntries>
      <TableRowEntry>
        <TableColumnItems>
          <TableColumnItem>
            <PropertyName>ComputerName</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>Address</PropertyName>
          </TableColumnItem>
<!--
          <TableColumnItem>
            <PropertyName>IPV4Address</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>IPV6Address</PropertyName>
          </TableColumnItem>
-->
          <TableColumnItem>
            <PropertyName>BufferSize</PropertyName>
          </TableColumnItem>
          <TableColumnItem>
            <PropertyName>ResponseTime</PropertyName>
          </TableColumnItem>
        </TableColumnItems>
      </TableRowEntry>
    </TableRowEntries>
  </TableControl>
</View>

ユーザー権限でやる

DotNetTypes.format.ps1xml というファイルはシステム ディレクトリにあって、ユーザー権限では書き換えることができません。
管理者権限であれば書き換えられますが、あまり推奨される方法ではありません。

というわけで、これをユーザー権限でできるようにしましょう。

まず空のテキストファイルを用意します。
そこに、IPv4Address と IPV6Address を削った XML をコピーします。
前後はルート要素で囲ってやる必要があります。

完成品がこちら。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <ViewDefinitions>
    <View>
      <Name>System.Management.ManagementObject#root\cimv2\Win32_PingStatus</Name>
      <ViewSelectedBy>
        <TypeName>System.Management.ManagementObject#root\cimv2\Win32_PingStatus</TypeName>
      </ViewSelectedBy>
      <TableControl>
        <TableHeaders>
          <TableColumnHeader>
            <Label>Source</Label>
            <Width>13</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Destination</Label>
            <Width>15</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Bytes</Label>
            <Width>8</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Time(ms)</Label>
            <Width>9</Width>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>__Server</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Address</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>BufferSize</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>ResponseTime</PropertyName>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
    <View>
      <Name>Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Win32_PingStatus</Name>
      <ViewSelectedBy>
        <TypeName>Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Win32_PingStatus</TypeName>
      </ViewSelectedBy>
      <TableControl>
        <TableHeaders>
          <TableColumnHeader>
            <Label>Source</Label>
            <Width>13</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Destination</Label>
            <Width>15</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Bytes</Label>
            <Width>8</Width>
          </TableColumnHeader>
          <TableColumnHeader>
            <Label>Time(ms)</Label>
            <Width>9</Width>
          </TableColumnHeader>
        </TableHeaders>
        <TableRowEntries>
          <TableRowEntry>
            <TableColumnItems>
              <TableColumnItem>
                <PropertyName>ComputerName</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>Address</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>BufferSize</PropertyName>
              </TableColumnItem>
              <TableColumnItem>
                <PropertyName>ResponseTime</PropertyName>
              </TableColumnItem>
            </TableColumnItems>
          </TableRowEntry>
        </TableRowEntries>
      </TableControl>
    </View>
  </ViewDefinitions>
</Configuration>

これを自分のドキュメント ディレクトリにある WindowsPowerShell ディレクト*3にでも置いておきましょう。

置いただけでは有効にならないので、Update-FormatData コマンドを打ってやります。
既定の表示定義を上書きするために、-PrependPath パラメーターに上記のパスを指定します。

PS> Update-FormatData -PrependPath .\Win32_PingStatus.format.ps1xml

こうしてから Test-Connection を実行すると、めでたくこうなります。

PS> Test-Connection localhost

Source        Destination     Bytes    Time(ms)
------        -----------     -----    --------
xxxxxxxxx     localhost       32       0
xxxxxxxxx     localhost       32       0
xxxxxxxxx     localhost       32       0
xxxxxxxxx     localhost       32       0

Update-FormatData を毎回タイプするのは面倒なので、PowerShell プロファイル*4に書いて、毎回自動実行されるようにしておきましょう。

その他の方法

上記の方法では、テーブル表示の場合だけ表示しないようにしているので、他の表示方法(Format-List とか)では消えません。
今回は敢えてそうしています。
ひょっとすると IP アドレスを知りたいことがあるかもしれないので、結果を詳細に表示するための Format-List には手を加えず、最も多く使われるであろう既定のテーブル表示のみを高速化すれば十分だろうと思ったためです。
リスト表示やその他の表示方法も、format.ps1xml を書き換えることで同様に可能です。
また、types.ps1xml でも表示するメンバーを制御することができます。
やり方は about_Format.ps1xml を見てください。

*1:PowerShell のインストール ディレクトリ ($PSHOME) にあります。

*2:types.ps1xml 同様、PowerShell のインストール ディレクトリにあります。

*3:標準では C:\Users\[ユーザー名]\Documents\WindowsPowerShell です。

*4:$PROFILE.CurrentUserAllHosts