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]
で最後の要素を取り出すことで、指定した行だけを読み取る」という使い方の例だ。実際に試してみる。
one
two
three
four
five
six
seven
PS > (Get-Content Cmdlets.txt -TotalCount 5)[-1]
five
確かに5行目が読み取れる。
ただし、このサンプルでは当然、Cmdlets.txtが5行未満の場合も考慮されず、単純に最後の行が返ってくる。
one
two
three
PS > (Get-Content Cmdlets_2.txt -TotalCount 5)[-1]
three
ここまではいい。問題は、テキストファイルの中身が1行のときだ。
one
PS > (Get-Content Cmdlets_3.txt -TotalCount 5)[-1]
e
!?
Why?
Get-Content
の結果をGetType
で確認する。
$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文字を返している。
$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
も配列展開も「気の利かせ方」が独特で覚えるのが大変……