はじめに
PowerShellで、コマンドの実行結果など、画面に表示される情報(テキスト)を利用して、パイプライン処理をする場合にOut-String
を使うことになるのですが、良くわからなかったのでいろいろ試してみました。
結論
★テキスト処理をする場合は、Out-String -Stream
(oss
)を挟む!
パイプラインの動作
- catの実行結果などは1行ずつ後段に渡される (String[])
-
Out-String -Stream
は1行ずつ後段に渡される (String[]) -
Out-String
は全部まとめて、最後に改行を付けて、後段に渡される (String)
注意点
- Stringで複数行が渡された場合は、1行毎に分解して処理する必要がある(要型変換)
- フォーマットされた文字列が渡された場合、
-Width
で文字数指定しないと、切れたり折り返されたりする場合がある- 結果が期待通りにならなかったら、(
.GetType()
などで入力オブジェクトの型を確認し、適切な)-Width
を指定してみる
- 結果が期待通りにならなかったら、(
ドキュメント
-
-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
の有無でオブジェクトのタイプが違うといいうことですね。
だから...
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オブジェクトがドーンと渡されると、厄介ですね。
だんだんわかってきたような気がします。
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
で上書き指定)が使われた
- ScriptBlockというタイプからStringタイプに型変換される際に、コンソール幅のパラメータ(
複数行のStringオブジェクトが渡されたら?
splitlines()のようなメソッドが見つからなかったので苦労しました。とりあえず、以下を挟めば複数行のオブジェクトに変換できました。
👎改善前
-
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 > ")
※ もっと良い方法があったら、ぜひ教えてください。
👍改善後
-
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
参考
おわりに
ちょっとしたメモのつもりが、長くなってしまいました。やはりPowerShellでのテキスト処理は難しいです。でもConvert-String
は例を与えるだけで処理してくれるので楽でした。正規表現を考えるのが面倒なときには、使ってみる価値がありそうです。
追記
現在のウィンドウ幅は、(Get-Host).UI.RawUI.WindowSize.Width
で知ることができます。
参考:PowerShellウインドーサイズをスクリプトで変更する