tech.guitarrapc.cóm

Technical updates

PowerShellでHashTableや配列の値をパイプラインでFunctionに渡す

今回は、HashTableや配列(一次、多次元)を、パイプラインでfunctionに渡してみたいと思います。
PowerShellでファイル名をソートして最後のアイテムを取得するなら

パイプラインで渡すとは

一応こういうイメージを意味しています。 ・HashTable
HashTable | function -param1 $_.PropertyX -param2 $_.PropertyY .....
・配列
配列 | function -param1 $_[0] -param2 $_[1] .....

実行するfunction

前回のfunctionを実行してみましょう。
function Get-FirstItem {

    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    [string]$Path,
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    [string]$Sortby,
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]
    [int]$Count
    )

    begin{
    }

    process{

        Get-ChildItem $Path | Sort-Object $SortBy | Select-Object -First $Count

    }

    end{
    }

}

HashTableをパイプラインでfunctionに渡す

これは簡単ですね。 HashTableの各Key毎にValueを渡すだけです。 例えば拡張子を指定します。
@{
    path=(Get-Location).Path;
    ext="Extention";
    count=1} `
    | %{ Get-FirstItem -Path $_.path -Sortby $_.ext -Count $_.count }
ファイル名を指定してみると?(敢えて変な書き方をしてみました。)
@{
    path=(Get-Location).Path;
    key=((Get-ChildItem | Get-Member -MemberType Property).Name | ? {$_ -like "Na*"});
    count=5} `
    | %{ Get-FirstItem -Path $_.path -Sortby $_.key -Count $_.count }
HashTableなので見やすい感があります。 HashTableは1Keyに1Valueなので、複数条件をまとめて実行するにはForeach-Obejctで囲むなり等が必要になります。 例えば、Get-ChildItemで指定可能な各プロパティごとにソートキーを指定してみます。
(Get-ChildItem | Get-Member -MemberType Property).Name `
    | %{
        @{
        path=(Get-Location).Path;
        key=$_;
        count=5} `
        | %{"sort by : $($_.key)"
            Get-FirstItem -Path $_.path -Sortby $_.key -Count $_.count
        }
    }
各プロパティごとに取得できました。
PS C:\hoge\fuga> D:\Document\Program\Powershell\Get-FirstItem\Get-FirstItem.ps1
sort by : Attributes


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 6.txt
-a--- 2013/02/19     20:06     16 5.txt
-a--- 2013/02/19     20:06     16 7.txt
-a--- 2013/02/19     20:06     16 9.txt
-a--- 2013/02/19     20:06     16 8.txt


sort by : CreationTime


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 1.txt
-a--- 2013/02/19     20:06     16 2.txt
-a--- 2013/02/19     20:06     16 3.txt
-a--- 2013/02/19     20:06     16 4.txt
-a--- 2013/02/19     20:06     16 5.txt


sort by : CreationTimeUtc


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 1.txt
-a--- 2013/02/19     20:06     16 2.txt
-a--- 2013/02/19     20:06     16 3.txt
-a--- 2013/02/19     20:06     16 4.txt
-a--- 2013/02/19     20:06     16 5.txt


sort by : Directory


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 6.txt
-a--- 2013/02/19     20:06     16 5.txt
-a--- 2013/02/19     20:06     16 7.txt
-a--- 2013/02/19     20:06     16 9.txt
-a--- 2013/02/19     20:06     16 8.txt


sort by : DirectoryName


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 6.txt
-a--- 2013/02/19     20:06     16 5.txt
-a--- 2013/02/19     20:06     16 7.txt
-a--- 2013/02/19     20:06     16 9.txt
-a--- 2013/02/19     20:06     16 8.txt


sort by : Exists


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 6.txt
-a--- 2013/02/19     20:06     16 5.txt
-a--- 2013/02/19     20:06     16 7.txt
-a--- 2013/02/19     20:06     16 9.txt
-a--- 2013/02/19     20:06     16 8.txt


sort by : Extension


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 6.txt
-a--- 2013/02/19     20:06     16 5.txt
-a--- 2013/02/19     20:06     16 7.txt
-a--- 2013/02/19     20:06     16 9.txt
-a--- 2013/02/19     20:06     16 8.txt


sort by : FullName


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 1.txt
-a--- 2013/02/19     20:06     18 10.txt
-a--- 2013/02/19     20:06     16 2.txt
-a--- 2013/02/19     20:06     16 3.txt
-a--- 2013/02/19     20:06     16 4.txt


sort by : IsReadOnly


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 6.txt
-a--- 2013/02/19     20:06     16 5.txt
-a--- 2013/02/19     20:06     16 7.txt
-a--- 2013/02/19     20:06     16 9.txt
-a--- 2013/02/19     20:06     16 8.txt


sort by : LastAccessTime


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 1.txt
-a--- 2013/02/19     20:06     16 2.txt
-a--- 2013/02/19     20:06     16 3.txt
-a--- 2013/02/19     20:06     16 4.txt
-a--- 2013/02/19     20:06     16 5.txt


sort by : LastAccessTimeUtc


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 1.txt
-a--- 2013/02/19     20:06     16 2.txt
-a--- 2013/02/19     20:06     16 3.txt
-a--- 2013/02/19     20:06     16 4.txt
-a--- 2013/02/19     20:06     16 5.txt


sort by : LastWriteTime


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 1.txt
-a--- 2013/02/19     20:06     16 2.txt
-a--- 2013/02/19     20:06     16 3.txt
-a--- 2013/02/19     20:06     16 4.txt
-a--- 2013/02/19     20:06     16 5.txt


sort by : LastWriteTimeUtc


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 1.txt
-a--- 2013/02/19     20:06     16 2.txt
-a--- 2013/02/19     20:06     16 3.txt
-a--- 2013/02/19     20:06     16 4.txt
-a--- 2013/02/19     20:06     16 5.txt


sort by : Length


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 6.txt
-a--- 2013/02/19     20:06     16 5.txt
-a--- 2013/02/19     20:06     16 7.txt
-a--- 2013/02/19     20:06     16 9.txt
-a--- 2013/02/19     20:06     16 8.txt


sort by : Name


    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 1.txt
-a--- 2013/02/19     20:06     18 10.txt
-a--- 2013/02/19     20:06     16 2.txt
-a--- 2013/02/19     20:06     16 3.txt
-a--- 2013/02/19     20:06     16 4.txt

一次配列をパイプラインでfunctionに渡す

一次配列をパイプを介して渡す際は少し注意です。

エラーが出るパターン

単純に渡そうとすると…配列が分解されてしまいエラーがでます。
# 配列が分解されるためErrorが出るパターン
((Get-Location).Path,"Name",1) | %{Get-FirstItem -Path $_[0] -Sortby $_[1] -Count $_[2]}
エラーが出ました。
Get-ChildItem : パス 'C:\hoge\fuga\C' が存在しないため検出できません。
発生場所 D:\Document\Program\Powershell\Get-FirstItem\Get-FirstItem.ps1:52 文字:9
+         Get-ChildItem $Path | Sort-Object $SortBy | Select-Object -Last $Count
+         ~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\hoge\fuga\C:String) [Get-ChildItem], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

Get-ChildItem : パス 'C:\hoge\fuga\C' が存在しないため検出できません。
発生場所 D:\Document\Program\Powershell\Get-FirstItem\Get-FirstItem.ps1:53 文字:9
+         Get-ChildItem $Path | Sort-Object $SortBy -Descending | Select-Object -L ...
+         ~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\hoge\fuga\C:String) [Get-ChildItem], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

Get-ChildItem : パス 'C:\hoge\fuga\N' が存在しないため検出できません。
発生場所 D:\Document\Program\Powershell\Get-FirstItem\Get-FirstItem.ps1:52 文字:9
+         Get-ChildItem $Path | Sort-Object $SortBy | Select-Object -Last $Count
+         ~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\hoge\fuga\N:String) [Get-ChildItem], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

Get-ChildItem : パス 'C:\hoge\fuga\N' が存在しないため検出できません。
発生場所 D:\Document\Program\Powershell\Get-FirstItem\Get-FirstItem.ps1:53 文字:9
+         Get-ChildItem $Path | Sort-Object $SortBy -Descending | Select-Object -L ...
+         ~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\hoge\fuga\N:String) [Get-ChildItem], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

Get-LastItem : 引数が空の文字列であるため、パラメーター 'Sortby' にバインドできません。
発生場所 D:\Document\Program\Powershell\Get-FirstItem\Get-FirstItem.ps1:207 文字:69
+ ((Get-Location).Path,"Name",1) | %{Get-LastItem -Path $_[0] -Sortby $_[1] -Count ...
+                                                                     ~~~~~
    + CategoryInfo          : InvalidData: (:) [Get-LastItem]、ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorEmptyStringNotAllowed,Get-LastItem
どういう事でしょうか。 単純にForeach-Objectで見てみましょう。
((Get-Location).Path,((Get-ChildItem).Extension | sort -Unique),1) | %{$_}
ふむ。
C:\hoge\fuga
Name
1
では、インデックスを指定してみます。
((Get-Location).Path,"Extension",1) | %{$_[0]}
StringがCharに分解されています。これがエラーの原因ですね。
C
N
1

一次配列をパイプの先に配列として渡す

対処は、一次配列の前にカンマ","をつけます。
,((Get-Location).Path,"Name",1) | %{Get-LastItem -Path $_[0] -Sortby $_[1] -Count $_[2]}
上手く渡せました。
    ディレクトリ: C:\hoge\fuga


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        2013/02/19     20:06         16 1.txt

多次元配列をパイプラインでfunctionに渡す

多次元配列をパイプを介して渡す際はそのままですね!
@(
    ((Get-Location).Path,"Extention",1),
    ((Get-Location).Path,"Name",2)
) `
    | %{Get-FirstItem -Path $_[0] -Sortby $_[1] -Count $_[2]}
中の配列毎にfunctionが実行されました。
    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 6.txt



    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 1.txt
-a--- 2013/02/19     20:06     18 10.txt

多次元配列をHashTableで受けてからfunctionに渡す

この辺は好き好きなので。 配列ではインデックス指定でキーで指定できません。(そりゃそうですし どうしてもキーで指定したい場合は、一度HashTableで受けれそうですね。 先ほどの要領なので軽く。
@(
    ((Get-Location).Path,"Extention",1),
    ((Get-Location).Path,"Name",2)
) `
    | %{
        @{
        path=$_[0];
        ext=$_[1];
        count=$_[2]}} `
    | %{ Get-FirstItem -Path $_.path -Sortby $_.ext -Count $_.count }
キーで指定できています。
    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 6.txt




    ディレクトリ: C:\hoge\fuga


Mode         LastWriteTime Length Name
----         ------------- ------ ----
-a--- 2013/02/19     20:06     16 1.txt
-a--- 2013/02/19     20:06     18 10.txt
簡単に、多次元配列をそのまま渡す場合と、一度HashTableの実行時間を計測してみました。(単位:Milliseconds)
#多次元配列をそのままパイプで渡す
1..10000 | %{
    Measure-Command{
        @(
            ((Get-Location).Path,"Extention",1),
            ((Get-Location).Path,"Name",2)
        ) `
            | %{Get-FirstItem -Path $_[0] -Sortby $_[1] -Count $_[2]}
    }
} | measure -Average TotalMilliseconds


#多次元配列をHashTableでうけてからパイプで渡す
1..10000 | %{
    Measure-Command{
    @(
        ((Get-Location).Path,"Extention",1),
        ((Get-Location).Path,"Name",2)
    ) `
        | %{
            @{
            path=$_[0];
            ext=$_[1];
            count=$_[2]}} `
        | %{ Get-FirstItem -Path $_.path -Sortby $_.ext -Count $_.count}
    }
} | measure -Average TotalMilliseconds
この例では約0.11 msの差ですね。
Count    : 10000
Average  : 3.37438835999998
Sum      :
Maximum  :
Minimum  :
Property : TotalMilliseconds

Count    : 10000
Average  : 3.48527187
Sum      :
Maximum  :
Minimum  :
Property : TotalMilliseconds

まとめ

PowerShellは、パイプでオブジェクトを渡すため、HashtTableの方が直感的に扱えます。 ただ、配列も渡せる方法を知っておくのもいいかと思います。

おまけ

またまた先生からアドバイスをいただきました。 ということでテスト。
function Get-FirstItem {

    [CmdletBinding()]
    param(
    [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
    [string]$path,
    [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
    [string]$ext,
    [Parameter(Mandatory=$true,ValueFromPipelineByPropertyName=$true)]
    [int]$count
    )

    begin{
    }

    process{

        Get-ChildItem $path | Sort-Object $ext | Select-Object -First $count

    }

    end{
    }

}
HashTableのパラメータ名も合わせてみると……
@{
    path="C:\hoge\fuga";
    ext="Extention";
    count=1} | Get-FirstItem
HashTableのままじゃダメみたいですねー。
Get-FirstItem : 入力オブジェクトをバインドできません。次のすべての必須パラメーターをバインドするために必要な情報がありませんでした:  path ext
発生場所 行:31 文字:16
+     count=1} | Get-FirstItem
+                ~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (System.Collections.Hashtable:Hashtable) [Get-FirstItem]、ParameterBindingException
    + FullyQualifiedErrorId : InputObjectMissingMandatory,Get-FirstItem
HashTableを[PSCustomObject]にキャストすればK,Vではなく、プロパティとして認識するので可能になります。
[PSCustomObject]@{
    path="C:\hoge\fuga";
    ext="Extention";
    count=1} | Get-FirstItem
    ディレクトリ: C:\hoge\fuga

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        2013/02/19     20:06         16 6.txt
この違いは、キャスト前とキャスト後を比べれば一目瞭然です。
@{
    path="C:\hoge\fuga";
    ext="Extention";
    count=1}

[PSCustomObject]@{
    path="C:\hoge\fuga";
    ext="Extention";
    count=1}
実行してみると
#HashTableのまま
Key   : path
Value : C:\hoge\fuga
Name  : path


Key   : ext
Value : Extention
Name  : ext


Key   : count
Value : 1
Name  : count

#HashTableを[PSCustomObject]にキャスト
path  : C:\hoge\fuga
ext   : Extention
count : 1
そりゃ駄目ですよね。勉強になりました。