PowerShell

PowerShellのGet-Contentでハマったところ

More than 1 year has passed since last update.

Get-Contentコマンドレットのオンラインヘルプには、以下のような例が載っている。(2017年11月20日確認)

Example 3: Get the fifth line of a text file

PS C:\> (Get-Content Cmdlets.txt -TotalCount 5)[-1]

This command gets the fifth line of the Cmdlets.txt text file. It uses the TotalCount parameter to get the first five lines and then uses array notation to get the last line (indicated by "-1") of the resulting set.

TotalCountオプションで読み込む行数を指定し、[-1]で最後の要素を取り出すことで、指定した行だけを読み取る」という使い方の例だ。実際に試してみる。

Cmdlets.txt
one
two
three
four
five
six
seven
PS > (Get-Content Cmdlets.txt -TotalCount 5)[-1]
five

確かに5行目が読み取れる。
ただし、このサンプルでは当然、Cmdlets.txtが5行未満の場合も考慮されず、単純に最後の行が返ってくる。

Cmdlets_2.txt
one
two
three
PS > (Get-Content Cmdlets_2.txt -TotalCount 5)[-1]
three

ここまではいい。問題は、テキストファイルの中身が1行のときだ。

Cmdlets_3.txt
one
PS > (Get-Content Cmdlets_3.txt -TotalCount 5)[-1]
e

!?

Why?

Get-Contentの結果をGetTypeで確認する。

check.ps1
$lines = Get-Content Cmdlets.txt
$lines.GetType()

$lines_2 = Get-Content Cmdlets_2.txt
$lines_2.GetType()

$lines_3 = Get-Content Cmdlets_3.txt
$lines_3.GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
True     True     Object[]                                 System.Array
True     True     String                                   System.Object

中身が1行しかないファイル(Cmdlets_3.txt)を読み取った場合は、戻り値が配列(Object[])ではなく文字列(String)になっている
よって(Get-Content Cmdlets_3.txt -TotalCount 5)[-1]は「文字列への添え字アクセス」となり、結果としてChar=1文字を返している。

check.ps1に追加
$lines[-1].GetType()
$lines_2[-1].GetType()
$lines_3[-1].GetType()
IsPublic IsSerial Name                      BaseType
-------- -------- ----                      --------
True     True     String                    System.Object
True     True     String                    System.Object
True     True     Char                      System.ValueType

よって、文字列「one」の最後の1文字である「e」が表示された。

解決法

@()を使い、戻り値を配列に格納すればよい。

$lines_3 = @(Get-Content Cmdlets_3.txt)
$lines_3.GetType()
$lines_3[-1].GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
True     True     String                                   System.Object

配列の展開

ここで、他のスクリプト言語の感覚だと「元々配列が返る場合は、配列の配列(ジャグ配列)になってしまうのでは?」と考えるのだけれど、どうやらPowerShellは要素数が1のジャグ配列を勝手に展開するようだ。

# ジャグ配列になっている例
$jagged = @(@(1, 2, 3), @(4, 5))
$jagged[0].GetType()
$jagged[1].GetType()

# ジャグ配列になっていない(1次元に展開される)例
$not_jagged = @(@(1, 2, 3))
$not_jagged[0].GetType()
$not_jagged[1].GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
True     True     Object[]                                 System.Array
True     True     Int32                                    System.ValueType
True     True     Int32                                    System.ValueType

$jaggedの各要素は配列だが、$not_jaggedの各要素には数値が直接入っている。
展開せず「要素数1のジャグ配列」を作るための書き方は下のようになるらしい。

# 要素数1のジャグ配列になっている例
$jagged_2 = @(,@(1, 2, 3)) # 先頭にカンマをつける
$jagged_2[0].GetType()
#$jagged_2[1].GetType() # 存在しない
$jagged_2[0][0].GetType()
$jagged_2[0][1].GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
True     True     Int32                                    System.ValueType
True     True     Int32                                    System.ValueType

結論

「ファイルの5行目を読み取る。5行未満の場合は最後の行を読み取る。」という仕様を意図するなら、以下のようになる。

PS > @(Get-Content Cmdlets_3.txt -TotalCount 5)[-1]
one

私見

  • 明示的に文字列で取得する-Rawオプションがあるのだから、そのオプションがないときは1行でも「要素数1の配列」で返した方がよくない?
  • この仕様が上手く活きるユースケースを思いつかない
    • 実際のスクリプトは、この問題を意識していないか、@(Get-Content ...)の書き方に統一しているかの2択なのでは
  • Get-Contentも配列展開も「気の利かせ方」が独特で覚えるのが大変……

参考