PowerShellのパイプラインのテキスト処理に必須なOut-Stringについて調べてみた

## はじめに
PowerShellで、コマンドの実行結果など、画面に表示される情報(テキスト)を利用して、パイプライン処理をする場合に`Out-String`を使うことになるのですが、良くわからなかったのでいろいろ試してみました。

## 結論
★テキスト処理をする場合は、`Out-String -Stream`(`oss`)を挟む！
### パイプラインの動作
1. catの実行結果などは1行ずつ後段に渡される (String[])
2. `Out-String -Stream`は1行ずつ後段に渡される (String[])
3. `Out-String`は全部まとめて、最後に改行を付けて、後段に渡される (String)

### 注意点

* Stringで複数行が渡された場合は、1行毎に分解して処理する必要がある(要型変換)
* フォーマットされた文字列が渡された場合、`-Width`で文字数指定しないと、切れたり折り返されたりする場合がある
  * 結果が期待通りにならなかったら、(`.GetType()`などで入力オブジェクトの型を確認し、適切な)`-Width`を指定してみる

## ドキュメント
[Out-String(Parameters)](https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.utility/out-string?view=powershell-5.1#parameters)

* `-Stream`の指定によって、複数行で渡すか、1行ずつ複数行を渡すかの違いがあるらしい。
* パイプラインに渡される、フォーマットされた1行の文字数がコンソールの幅よりも長い場合は、`-Width`で文字数を指定しないと、折り返されたり切られてしまうことがあるらしい。

## 試したこと
### テスト用テキストファイル
```
PS > cat .\unsorted.txt
この行が1行目にあったら、Sortされていません。(Sort後は最終行)
3. Out-Stringは全部まとめて、最後に改行を付けて、後段に渡す
2. Out-String -Streamは1行ずつ後段に渡す
1. catの実行結果などは1行ずつ後段に渡す
```

### 普通にsortする
```
PS > cat .\unsorted.txt | sort
1. catの実行結果などは1行ずつ後段に渡す
2. Out-String -Streamは1行ずつ後段に渡す
3. Out-Stringは全部まとめて、最後に改行を付けて、後段に渡す
この行が1行目にあったら、Sortされていません。(Sort後は最終行)
```

### `Out-String`を挟む
```
PS > cat .\unsorted.txt | Out-String | sort
この行が1行目にあったら、Sortされていません。(Sort後は最終行)
3. Out-Stringは全部まとめて、最後に改行を付けて、後段に渡す
2. Out-String -Streamは1行ずつ後段に渡す
1. catの実行結果などは1行ずつ後段に渡す

```
sortされずに、最後に空行が付きました。

### `Out-String -Stream`を挟む
```
PS > cat .\unsorted.txt | Out-String -Stream | sort
1. catの実行結果などは1行ずつ後段に渡す
2. Out-String -Streamは1行ずつ後段に渡す
3. Out-Stringは全部まとめて、最後に改行を付けて、後段に渡す
この行が1行目にあったら、Sortされていません。(Sort後は最終行)
```

### ossとは?
```
PS > help oss

名前
    Out-String

構文
    Out-String  [<CommonParameters>]
[...]
```
良くわからない。

```
PS >  (gi function:oss).ScriptBlock | Out-String -Stream | sls Stream

        $PSBoundParameters['Stream'] = $true

```
`-Stream`付きと等価らしい。

余談：関数定義を`.Definition`で取得すると、フィルタ処理ができませんでした。そこで、何が違うか調べてみました。

```
PS >  (gi function:oss).ScriptBlock.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     ScriptBlock                              System.Object


PS >  (gi function:oss).Definition.GetType()

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

```
ということは...

```
PS >  ((gi function:oss).ScriptBlock| Out-String).GetType()

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


PS >  ((gi function:oss).ScriptBlock| Out-String -Stream).GetType()

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


```
`-Stream`の有無でオブジェクトのタイプが違うといいうことですね。<br>
だから...

```
PS >  ((gi function:oss).ScriptBlock| Out-String)[0].GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Char                                     System.ValueType


PS >  ((gi function:oss).ScriptBlock| Out-String -Stream)[0].GetType()

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


```
`-Stream`を付けるとStringの配列というオブジェクトが渡されるので、1行毎の処理が後段でできるようになるということです。`-Stream`なしで複数行のStringオブジェクトがドーンと渡されると、厄介ですね。<br>
だんだんわかってきたような気がします。

### `oss`を挟む

```
PS > cat .\unsorted.txt | oss | sort
1. catの実行結果などは1行ずつ後段に渡す
2. Out-String -Streamは1行ずつ後段に渡す
3. Out-Stringは全部まとめて、最後に改行を付けて、後段に渡す
この行が1行目にあったら、Sortされていません。(Sort後は最終行)
```
`oss`は`Out-String -Stream`と同じだと考えて良さそうです。

### フォーマットされた文字列が途中改行される例
```
PS > ((gi function:toggle_prompt).ScriptBlock | oss)

        if ((prompt) -eq "PS > ")
                {function global:prompt {"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($n
estedPromptLevel + 1)) "}}
        else
                {function global:prompt {"PS > "}}

PS > ((gi function:toggle_prompt).ScriptBlock | oss -w 122)

        if ((prompt) -eq "PS > ")
                {function global:prompt {"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "}}
        else
                {function global:prompt {"PS > "}}

PS > ((gi function:toggle_prompt).ScriptBlock | oss).count
7
PS > ((gi function:toggle_prompt).ScriptBlock | oss -w 122).count
6
```
* 普通にコンソールを開くと1行が80文字です。タブは1文字カウント。
* `.ScriptBlock`はフォーマット指定して出力しているということがわかります。
  * ScriptBlockというタイプからStringタイプに型変換される際に、コンソール幅のパラメータ(`-Width`で上書き指定)が使われた

### 複数行のStringオブジェクトが渡されたら?
splitlines()のようなメソッドが見つからなかったので苦労しました。とりあえず、以下を挟めば複数行のオブジェクトに変換できました。

#### &#x1f44e;改善前
* ``ConvertFrom-String -Delimiter "`n" | oss -Width ＜文字数＞ | Convert-String -Example "P1:left=left"``
  * `＜文字数＞`部分は適宜指定(わからなかったら`999`などでも可)

例：

```
PS > (gi function:toggle_prompt).definition | sort                                                        
        if ((prompt) -eq "PS > ")
                {function global:prompt {"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "}}
        else
                {function global:prompt {"PS > "}}

PS > (gi function:toggle_prompt).definition | cfs -d "`n" | oss -w 128 | Convert-String -e "P1:left=left" | sort


                {function global:prompt {"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "}}
                {function global:prompt {"PS > "}}
        else
        if ((prompt) -eq "PS > ")
```
~~※ **もっと良い方法があったら、ぜひ教えてください。**~~
#### &#x1f44d;改善後
* `oss`の代わりに``%{$_.split("`n")}``を使う

例：

```
PS > (gi function:toggle_prompt).definition | %{$_.split("`n")} | sort

                {function global:prompt {"PS $($executionContext.SessionState.Path.CurrentLocation)$('>' * ($nestedPromptLevel + 1)) "}}
                {function global:prompt {"PS > "}}
        else
        if ((prompt) -eq "PS > ")

```

@ktz_alias さん、アドバイスありがとうございました。

### 確認環境
* Windows 10 Home 21H2

```
PS > (gi function:ver).definition
(systeminfo | ?{$_.Contains("OS")})[0,1];($PSVersionTable | oss)[3].trim() -replace ' +',":`t`t"
PS > ver
OS 名:                  Microsoft Windows 10 Home
OS バージョン:          10.0.19044 N/A ビルド 19044
PSVersion:              5.1.19041.1320
```

## 参考
* [ConvertFrom-String](https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.utility/convertfrom-string?view=powershell-5.1)
* [Convert-String](https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.utility/convert-string?view=powershell-5.1)
* [PowerShellシステム要件、バージョン情報メモ](https://qiita.com/e4rfx/items/e370ba343a3c3841b646)
* [PowerShellのプロンプト切り替え関数](https://qiita.com/e4rfx/items/c75843d373ce7f5223ea)

## おわりに
ちょっとしたメモのつもりが、長くなってしまいました。やはりPowerShellでのテキスト処理は難しいです。でも`Convert-String`は例を与えるだけで処理してくれるので楽でした。正規表現を考えるのが面倒なときには、使ってみる価値がありそうです。

### 追記
現在のウィンドウ幅は、`(Get-Host).UI.RawUI.WindowSize.Width`で知ることができます。
参考：[PowerShellウインドーサイズをスクリプトで変更する](http://powershell.web.fc2.com/Html/Data/2010/09/29/0001.html)