PowerShellでコンソールアプリ出力(複数行Null文字交じりの文字列データ)をCSVを経由してGridViewに表示する(Windows Subsystem for Linuxの稼働監視を例に)

## 頑張って整形してるコンソールアプリの出力、GridViewで表示したい

内容はタイトルまんまです。
Windows Subsystem for Linuxをいろいろいじくり倒していた際に
```powershell
wsl.exe --list --verbose
```
でディストリビューションの稼働状況を見ていたのですが、ふと「これGridViewで見れればなぁ」と思ったのでやってみたところ意外と手こずったので記事にしました。

## 検証環境
||バージョン|
|:-:|--:|
|Operating System|Windows 11|
|Windows Subsystem for Linux|1.0.3.0|
|PowerShell|7.3.1|

```text:wsl.exe --versionの結果
wsl.exe --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
MSRDC バージョン: 1.2.3575
Direct3D バージョン: 1.606.4
DXCore バージョン: 10.0.25131.1002-220531-1700.rs-onecore-base2-hyp
Windowsバージョン: 10.0.22621.1194
```
```text:$PSVersionTableの内容
Name                           Value
----                           -----
PSVersion                      7.3.1
PSEdition                      Core
GitCommitId                    7.3.1
OS                             Microsoft Windows 10.0.22621
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
```


## 1. Null文字を除去しよう

一番最初につまづいたのはPowerShellコンソール上で見ている文字列と実際に出力された文字列が違ったことです。
何で気づいたかってCSVにしたくて頑張って置換処理を書いていたんですが、思った通りに動かなかったのでよくよく見返したからなんですけれどもね。
結論から言うと各文字の間にNull文字が入ってました。

解説すると、例えばNull文字を``''``として

```text:本当はこうなっている
''P''o''w''e''r''s''h''e''l''l<CR><LF>
```
という文字列がコンソールに渡されたとしても、表示上は

```powershell:Windows Terminal(PowerShell)上での表示
PowerShell
```
と見えます。

テキストエディタに渡したりして見てみるとやっと
```text:1文字ずつ文字の前にNull文字が入ってる
<NULL>P<NULL>o<NULL>w<NULL>e<NULL>r<NULL>s<NULL>h<NULL>e<NULL>l<NULL>l<CR><LF>
```
とNull文字があることに気づきます。

ですので、コンソールアプリの出力からNull文字(文字コードだとchar(00))を除去してやります。
本記事では、例としてwls.exeで導入済みのディストリビューションを一覧表示する場合を取り扱います。

具体的な処理自体は単なる置換処理で
```powershell:出力からNull文字を削除する
(wsl.exe --loist --verbose) -replace([char](0x00),'')
```
と出力そのものに対して-replace()を使用してNull文字を「何もない」文字と置換します。

ところでこの出力はそもそも``-replace()``かけられるの?という疑問も出てきます。
それについては
```powershell:コンソールアプリの出力に型判定を実施した結果
# まずはオブジェクト全体の型を確認
# ->この時点では「配列」ということまでしかわからない
(wsl.exe --list --verbose).GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

# 配列ならば必ず先頭があるだろうという前提で[0]番目の型を確認した結果
# -> String型であることが確認できる
(wsl.exe --list --verbose)[0].GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     String                                   System.Object
```

というように複数行で出力されるものであれば``System.Array``型の中に``System.String``型が入っている
つまり``[System.String][]array``をコンソールは受け取ることになります。
したがって、Stringクラスに対してreplaceメソッドを適用するという一般的な処理でNull文字の除去が可能となります。

## 2. 空行(改行しかない行)を除去しよう
うまい具合に``Out-GridView``コマンドで表形式の表示にするためには

  1. 空行(特に列数が一致しない空行)が除去されていること
  1. そこそこ正しいCSVになっていること(最低限、各行の列数が一致していること)

が必要です。
ここでは1.について考えます。

先ほど「コンソールアプリの出力がString型の配列だ」(これ前提条件)というのがわかっのたで、各行に対して「もし空だったらその行は無きものにしてね」という処理を書けばよいわけです。
ここでは先人の知恵をお借りして<sup>[[1]][link-1] [[2]][link-2] [[3]][link-3] [[4]][link-4]</sup>さくっと書いていきます。
```powershell:Null除去したオブジェクトから空行を除去
(wsl.exe --list --verbose) -replace([char](0x00),'') |
  Where-Object {${_}.trim() -ne ""}
```

パイプライン処理でNull除去したオブジェクトをそのまま渡して``""``(空白行)ではないものを返しています。
ここで、単純にやるのであれば``Where-Object {${_} -ne "}``とするところですが、上の2.を満たすため空白のみの行もtrimコマンドで空行に変更してから処理を実施しています。

## 3. 出力中の「空白の連続」を「カンマ区切り」に編集しよう<br>　(※ただし改行はそのままにすること)
ここまででパイプライン出力にはヘッダー行とデータ項目のみが存在するデータになりました。
今度は各行に対して空白(または空白の連続)を``","``区切りにする処理をかけていきます(CSV: Comma-Separated Values 形式にしたい)。これはさほど難しくありません。繰り返し処理と正規表現で「空白の1個以上の連続」をすべて``","``に置き換えます。
```powershell: 空行除去までした結果に対する空白と","の置換処理
(wsl.exe --list --verbose) -replace([char](0x00),'') |
  Where-Object {$_.trim() -ne "" } |
    ForEach-Object -Process { $_ -replace "\s+", "," } |
```
[正規表現][link-5]<sup>[[5]][link-5]</sup>に関しては[とほほの正規表現入門][link-6]<sup>[[6]][link-6]</sup>を見ておけばまず間違えないかと思います。
極めるなら[O'Reilyのフクロウ本][link-7]<sup>[[7]][link-7]</sup>を参照するとよいかもしれません。

## 4. CSV形式に加工した配列をオブジェクトに変換しよう
ここまでくれば、あとはCSVに加工したString型配列をオブジェクトに変換してGridViewにパイプライン処理で渡すだけです。
ここではCSV文字列からなる配列をGridViewが扱うことのできるオブジェクトに変換する方法を確認します。
といってもやることは``ConvertFrom-Csv``コマンドにCSV形式の文字列を渡して次のパイプライン処理へ出力を渡せばおしまいです。
具体的なコードは次のようになります。

```powershell:ConvertFrom-Csvで出力結果をオブジェクトに変換する
(wsl.exe --list --verbose) -replace([char](0x00),'') |
  Where-Object {$_.trim() -ne "" } |
    ForEach-Object -Process { $_ -replace "\s+", "," } |
      ConvertFrom-Csv | # <-ここでCSVの("a","b","c",...)というのがPS-Objectに変換される
        Out-Host        # <-この Out-Host がパイプラインで受け取る入力(ConvertFrom-Csvの出力)がオブジェクトになっている
```

ここで``ConvertFrom-CSV``に渡す文字列が「完全なCSV」であれば何も起こらないのですが、例えば表1のような(今回のお題であるwsl.exeの出力)の場合はコンソールに警告が出てきます。今回取り扱うデータには※のセルのデータがありません。

<table>
  <caption>表1 ヘッダーが欠損しているCSVデータ</caption>
  <thead>
    <tr>
      <th>※</th><th>NAME</th><th>STATE</th><th>VRSION</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>*</td><td>Ubuntu</td><td>Stopped</td><td>2</td>
    </tr>
    <tr>
      <td></td><td>Ubuntu-22.04</td><td>Stopped</td><td>2</td>
    </tr>
  </tbody>
</table>

CSVにするとこうなります。
```text:表1をCSV形式のテキストデータで表現した内容
※,NAME,STATE,VRSION<CR><LF>*,Ubuntu,Stopped,2<CR><LF>Ubuntu-22.04,Stopped,2
```

実行するとエラー表示が出ます。
(画像を取るのがめんどいので、以下、コンソール出力についてはコピー＆ペーストで結果を貼り付けます。)

```powershell:実行結果
(wsl.exe --list --verbose) -replace([char](0x00),'') |
>   Where-Object {$_.trim() -ne "" } |
>     ForEach-Object -Process { $_ -replace "\s+", "," } |
>       ConvertFrom-Csv | Out-Host -replace "\s+", "," } |
# 以下が出力。
# WARNINGで「ヘッダーが定義されてないよ!!代わりに"H"付きで勝手につけたからね!!」と怒られてる。
WARNING:
One or more headers were not specified. 
Default names starting with "H" have been used in place of any missing headers.

H1 NAME         STATE   VERSION
-- ----         -----   -------
*  Ubuntu       Stopped 2
   Ubuntu-22.04 Stopped 2
```

それほど使うわけでなければWARNINGのメッセージがでても良いのですが、できることなら気にせず行きたいところです。
そこで、エラーを握りつぶしてコンソールをきれいにするために``ConvertFrom-CSV -WarningAction SilentlyContinue``とオプションをつけて実行します。

```powershell:オプションに-WarnngAction SilentlyContinueを指定して実行した結果
(wsl.exe --list --verbose) -replace([char](0x00),'') |
>   Where-Object {$_.trim() -ne "" } |
>     ForEach-Object -Process { $_ -replace "\s+", "," } |
>       ConvertFrom-Csv -WarningAction SilentlyContinue | Out-Host
H1 NAME         STATE   VERSION
-- ----         -----   -------
*  Ubuntu       Stopped 2
   Ubuntu-22.04 Stopped 2
```

このように実行結果の前に表示されていた警告を非表示にできます。
特に、ログ収集などで「特定の場合にスクリプトがエラーを吐くのはわかっているんだけれどもそれは出力させたくない」という場合などに応用できます。
[PowerShellのエラー表示][link-8]<sup>[[8]][link-8]</sup>と[WariningActionに設定できる値][link-9]<sup>[[9]][link-9]</sup>については[参考文献8][link-8]、[参考文献9][link-9]をそれぞれ参照してください。


## 5. 結果をGridViewで表示しよう
1.～4. まででデータの取得・クリーニング・CSVへの整形・オブジェクトへ変換とパイプライン処理をつないできました。
ここまでくればGridViewへ出力できます。やり方はいたって単純で、パイプライン処理の結果を``Out-GridView``へパイプ渡ししてやります。

```powershell:実行結果をGridViewで表示
(wsl.exe --list --verbose) -replace([char](0x00),'') |
  Where-Object {$_.trim() -ne "" } |
    ForEach-Object -Process { $_ -replace "\s+", "," } |
      ConvertFrom-Csv -WarningAction SilentlyContinue |
        Out-GridView -Title "List of WSL Status"
```

出力はこんな感じになります。
![wsl.exeて取得したディストリビューションの稼働状態(GridView)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/352775/4b01fb0f-6ec5-5aad-0be0-42c9bb1e7b76.png "wsl.exeて取得したディストリビューションの稼働状態(GridView)")

以上、扱いづらいデータであってもCSV形式を経由して、いい感じにGUIのテーブル形式で表示できるようになりました。

## 番外. Microsoft.PowerShell_Profile.ps1に関数として登録する
1.～5.までパイプライン処理を用いてGUIで状況を見られるようにしました。
でもこんなの一々打ち込んでられないので、私はPowerShellのプロファイル(Linuxの``.[hoge]shrc``に相当するファイル $PROFILEにパスが格納されている)に関数を作って一発でだせるようにしています。

以下、抜粋です。
```powershell:Get-WslStatus (WSLの稼働状態を表示)
Filter Get-WslStatus
{
<#
.SYNOPSIS
WSLの稼働状況を取得してグリッドビューに表示します。

.DESCRIPTION
wsl.exe --list --verboseした結果をCSV形式で構造化した上で
グリッドビューにパイプラインで渡して表示します。
#>
(wsl.exe --list --verbose) -replace([char](0x00),'') |
  Where-Object {$_.trim() -ne "" } |
    ForEach-Object -Process { $_ -replace "\s+", "," } |
      ConvertFrom-Csv -WarningAction SilentlyContinue |
        Out-GridView -Title "List of WSL Status"
}

```
頭にコメントでヘルプメッセージを書いておくと、後で「これなんだっけ?」となった際や、ほかに人に渡した際に「ヘルプ参照してください」で済むので便利です。

何かの参考になれば幸いです。


## 参考文献
1\. [PowerShell 配列の空白要素を削除する][link-1]

  https://qiita.com/ryemug1/items/25bed2d8d77e2865fa4d

2\. [BASHとPowerShellのフィルタ処理まとめ③][link-2]

  https://qiita.com/HOBO0222/items/abaf25087de429e76c04

3\. [【PowerShell】出力結果から空白行を削除][link-3]

  https://www.livefree.jp/powershell_memo01/

4\. [パイプラインからのオブジェクトの削除][link-4]

  https://learn.microsoft.com/ja-jp/powershell/scripting/samples/removing-objects-from-the-pipeline--where-object-

5\. [正規表現: wikipedia][link-5]

https://ja.wikipedia.org/wiki/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE

6\. [とほほの正規表現入門][link-6]

https://www.tohoho-web.com/ex/regexp.html

7\. [Jeffrey E.F. Friedl 著, 長尾 高弘 訳,"詳説 正規表現 第3版", O'Reily Japan, 2008][link-7]

https://www.oreilly.co.jp/books/9784873113593/

8\. [[Microsoft Learn] Write-Warning][link-8]

https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.utility/write-warning

9\. [[Microsoft Learn] about_CommonParameters - 長い説明][link-9]

https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_commonparameters#long-description


[link-1]:https://qiita.com/ryemug1/items/25bed2d8d77e2865fa4d
[link-2]:https://qiita.com/HOBO0222/items/abaf25087de429e76c04
[link-3]:https://www.livefree.jp/powershell_memo01/
[link-4]:https://learn.microsoft.com/ja-jp/powershell/scripting/samples/removing-objects-from-the-pipeline--where-object-
[link-5]:https://ja.wikipedia.org/wiki/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE
[link-6]:https://www.tohoho-web.com/ex/regexp.html
[link-7]:https://www.oreilly.co.jp/books/9784873113593/
[link-8]:https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.utility/write-warning
[link-9]:https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_commonparameters#long-description