![]() |
ええ…(困惑) |
思った以上に癖の強い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
2
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だ!!
さて、何が起こったのか考えてみよう。
>> aaa=1
>> bbb=2
>> ccc=3
>> ddd=4
>> "@ | ConvertFrom-StringData
PS C:\work> $hashTable.bbb
2
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
# 一行ずつ変換してっても同じ結果になるけど処理コスト高そう
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、便利は便利だけど仕様がいろいろ理不尽だと思う。簡単にいうと、ConvertFrom-StringDataはStringを受け取った時にHashtableを返し、Arrayを受け取るとArrayを返すということ。— シダモ (@coffee_smoke73) 2018年5月24日
このときのArrayは一見Hashtableっぽい挙動をするからたちが悪い。実際Addメソッドがエラーになって初めて気付いた(PowerShellのArrayは固定長)#PowerShell
0 件のコメント :
コメントを投稿