【PowerShell】 ConvertFrom-StringDataが返すのはHashtableとは限らない!

【PowerShell】 ConvertFrom-StringDataが返すのはHashtableとは限らない!

うそつき!!
ええ…(困惑)

思った以上に癖の強いConvertFrom-StringData

前回ちらっと触れたConvertFrom-StringDataコマンドレット。
いじってたらへんてこな挙動に気付いてしまった。これは酷い初見殺し。
問題の詳細と解決策についてメモしてみる。


まず、公式情報を見てみよう。

Key/Valueペアを含むSystem.Stringを渡すとSystem.Collections.Hashtableを返しますよ、と書いてある。
これは間違いない。ヒアストリングで渡すと問題なく動作する。

次、Key/Valueペアを含むテキストファイルをGet-Contentで読み込んでパイプで渡してみる。
Hashtableのように要素にアクセスできる。一見問題なく見える。
ところが、Addメソッドで要素を追加しようとすると怒られる!
型を確認してみると…Array?どういうことなの?

コマンドラインで見てみると次のような感じ。
PS C:\work> $hashTable = Write-Output @" 
>> aaa=1 
>> bbb=2 
>> ccc=3 
>> ddd=4 
>> "@ | ConvertFrom-StringData 

PS C:\work> $hashTable.bbb 


PS C:\work> $hashTable.add("eee",5) 
PS C:\work> $hashTable
 Name Value 
---- ----- 
eee 5 
ccc 3 
ddd 4 
bbb 2 
aaa 1

PS C:\work> $hashTable.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------

True     True     Hashtable                                System.Object

# ヒアストリングでStringそのものを渡すと何も問題ない
# ところがGet-Contentで読み取ったものを直接ConvertFrom-StringDataに渡すと

PS C:\work> Write-Output "aaa=1" "bbb=2" "ccc=3" "ddd=4"| Out-File hoge.txt
PS C:\work> Get-Content hoge.txt
aaa=1
bbb=2
ccc=3
ddd=4
PS C:\work> $hashTable? = Get-Content hoge.txt | ConvertFrom-StringData
PS C:\work> $hashTable?.bbb
2
PS C:\work> $hashTable?.Add("eee",5)
"add" のオーバーロードで、引数の数が "2" であるものが見つかりません。
発生場所 行:1 文字:1
+ $hashTable?.Add("eee",5)
+ ~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodCountCouldNotFindBest

PS C:\work> $hashTable?.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------

True     True     Object[]                                 System.Array

# Get-Contentで読み取ったものを直接ConvertFrom-StringDataに渡すと
# 一見Hashtableっぽく動いてるけどAddメソッドでエラーになる
# 型を確認するとArrayだ!!
さて、何が起こったのか考えてみよう。
まず、Get-Contentコマンドレットだけど、これはデフォルトだと読み込んだファイルを
改行の位置で区切って配列に変換する。
つまり、Get-Contentから直接ConvertFrom-StringDataに出力を渡すと、
渡されるオブジェクトはStringではなくArrayになる。

そして問題のConvertFrom-StringDataコマンドレットだけど、
公式情報にもあるようにこいつの役目はあくまでStringを受け取りHashtableを返すこと。
Arrayを渡されるとどうなるかの説明はない。
実際にArrayを渡すと、Hashtableっぽく動くArrayを返すよう。

挙動はHashtableっぽいけどあくまでArray。
そしてArrayはAddメソッドを持っていないから、Addしようとすると怒られるよう。
※PowerShellのArrayは固定長

※2018/5/25追記
これ、正体をちゃんと確認してみたら中にHashtableの入っているArrayだった。
なので、$hashTable?[0].Add とかやれば中身のHashtableのAddメソッドを使うこともできる。
まあ、変なネスト構造にしてしまうと扱いづらいのであまりやらないほうがいいとは思う。

さて、解決方法だけど、
簡単なのはGet-Contentコマンドレットの-Rawオプションを使うこと。
これで、改行のあるファイルを配列ではなく改行つきのStringとして扱うことができる。

でも、ファイルから読み取った情報を加工してからHashtable化したい場合はどうしよう。
PowerShellは文字列を配列として扱うような機能が多いので、
改行つき文字列のままでは非常に扱いづらい。簡単なフィルタ操作にも難儀する。

そこでArray→Hashtableの変換をしたくなるわけだけど、
これは単純なキャストはできないのでひと工夫が必要になる。

具体的な流れを見てみよう。
PS C:\work> Get-Content hoge.txt
aaa=1
bbb=2
ccc=3
ddd=4
PS C:\work> $hashTableDayo = Get-Content hoge.txt -Raw | ConvertFrom-StringData
PS C:\work> $hashTableDayo.GetType()

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

# -Rawオプションをつけると無事Hashtableになった!

PS C:\work> [Hashtable]$hashTable?
"System.Object[]" の値を "System.Object[]" 型から "System.Collections.Hashtable" 型に変換できません。
発生場所 行:1 文字:1
+ [Hashtable]$hashTable?
+ ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) []、RuntimeException

    + FullyQualifiedErrorId : ConvertToFinalInvalidCastException

# Array→Hashtableのキャストは怒られる

PS C:\work> $array = Get-Content hoge.txt
PS C:\work> $array
aaa=1
bbb=2
ccc=3
ddd=4
PS C:\work> [String]$array
aaa=1 bbb=2 ccc=3 ddd=4

# ArrayをStringにキャストすると改行区切りじゃなくてスペース区切りになってしまう

PS C:\work> $string = $array -join "`r`n"
PS C:\work> $string
aaa=1
bbb=2
ccc=3
ddd=4
PS C:\work> $string.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------

True     True     String                                   System.Object

# -join演算子を使えばArrayを改行つきの文字列に変換できる
# これでHashtable化できる!

PS C:\work> $array | ForEach-Object { $hashTableDesuyo += ConvertFrom-StringData $_  }
PS C:\work> $hashTableDesuyo

Name                           Value
----                           -----
ddd                            4
bbb                            2
ccc                            3
aaa                            1


PS C:\work> $hashTableDesuyo.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------

True     True     Hashtable                                System.Object

# 一行ずつ変換してっても同じ結果になるけど処理コスト高そう
解決策はわかったけど、この挙動結構問題だと思う。
何がまずいかって、本来Stringを受け取るべきところでArrayを受け取った際、
エラーを返すのではなくて似て非なる別物を返していること。
実際、Addメソッドが失敗するまで何が起こったのか気付かなかった。
普段は型管理がふわふわしてるくせにこういう時だけシビアな操作を求めるのも酷い。
PowerShell、便利は便利だけど仕様がいろいろ理不尽だと思う。

0 件のコメント :

コメントを投稿