1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2022-01-30

はじめに

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)

  • -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後は最終行)

ossOut-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()のようなメソッドが見つからなかったので苦労しました。とりあえず、以下を挟めば複数行のオブジェクトに変換できました。

👎改善前

  • 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ウインドーサイズをスクリプトで変更する

1
1
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?