Windows PowerShellのFunction入門 (自動変数編)

この記事はWindows PowerShellのFunctionが引数を受け取る際、自動変数をの$args$_$inputを使って読み出す話です

書いておいてなんですが (というか書いて実感したのですが)、Begin/Process/Endブロックを書かない (Processブロックのみの) Filterを書く時に$_を使い、それ以外の場合は自動変数を使わずにParamキィワードを使ったほうが良いです


引数名を指定せずに渡した値は$argsに配列として格納されます

Function SampleFunction1 {
    $args.GetType().Name;
    $args.Length;
    $args[0];
    $args[0].GetType().Name;
    $args[1];
    $args[1].GetType().Name;
}

PS> SampleFunction1 'a'  1;
Object[]
2
a
String
1
Int32

Pipelineから渡された値の受け取りは若干複雑です

Processブロックがある場合、$_にその時のpipelineから渡された値が格納されます

$_Processブロック開始前 (Beginブロック内) には存在していませんが、Endブロックで読み出せます (必然的にpipelineから渡された最後の引数が格納されています)

Function SampleFunction2 {
    Begin{
        'Begin';
        # $_; 実行するとエラーになる
    }
    Process{
        'Process';
        $_;
    }
    End{
        'End';
        $_;
    }
}

PS> 1,2,3 | SampleFunction2
Begin
Process
1
Process
2
Process
3
End
3

次に、pipelineの値はArrayList+ArrayListEnumeratorSimpleとして$inputに格納されます

Windows PowerShellではArrayList+ArrayListEnumeratorSimpleは.NETのIEnumerator インターフェイス (System.Collections)として実装されています

Windows PowerShell: These members are defined in the interface System.IEnumerator, which is implemented by the types identified below.

(中略)

Windows PowerShell: For $input, this type is System.Collections.ArrayList+ArrayListEnumeratorSimple.

$inputBeginブロックでも読み出せますが値は入っておらず、何も入っていません

# 値は読み出せないが存在はしているので GetType() で型の名前は参照できる
Function SampleFunction3 {
    Begin{
        $input;
        $_.GetType().Name;
    }
}

PS> 1,2 | SampleFunction3
ArrayListEnumeratorSimple

Processブロックがあった場合、$_と同じ値を読み出せますが、1回読み出すと読み出せなくなります

Function SampleFunction4 {
    Process{
        $_;
        $input;
        $input;
    }
}
PS> 1,2 | SampleFunction4
1
1
2
2

ProcessブロックがなくEndブロックがあった場合、$inputは全てのpipelineから渡された値を配列として持っていますが、1回読み出すと読み出せなくなります

Function SampleFunction5 {
    End{
        $input;
    }
}

Function SampleFunction6 {
    End{
        $input;
        $input; # 2回目は読み出せないので、1回のみの場合と結果が同じになる
    }
}

PS> 1,2 | SampleFunction5
1
2
PS> 1,2 | SampleFunction6
1
2

ProcessブロックとEndブロックがあった場合、Processブロック内で読み出したかどうかに関わらず消費され、Endブロックでは読み出せなくなっています

Function SampleFunction7 {
    Process{}
    End{
        Write-Host 'End';
        $input; # Processブロックがあるため何も読み出せない
    }
}

PS> 1,2 | SampleFunction7
End

ここまで長々説明しておいてなんですが、$_Begin/Process/Endブロックを書かない (Processブロックのみの) Filterをさっと書いてその場で処理する場合には便利ですが、$inputは癖があり、メンテナンス時にバグを作りこみそうな気がしますので自動変数は極力使わないのがよかろうと思います

PowerShellのOutputType属性を調べたので覚書

自分は他人が書いたPowerShellのコードをあまり読む機会がなく思わぬところで基礎的な部分が抜けていたりします

で、ごく最近になってからOutputType属性を知ったので、それを調べた時の覚書です


まず、about_Functions_OutputTypeAttribute | Microsoft Docsを読んでみます

The OutputType attribute lists the .NET types of objects that the functions returns. You can use its optional ParameterSetName parameter to list different output types for each parameter set.

The OutputType attribute is supported on simple and advanced functions. It is independent of the CmdletBinding attribute.

The OutputType attribute provides the value of the OutputType property of the System.Management.Automation.FunctionInfo object that the Get-Command cmdlet returns.

The OutputType attribute value is only a documentation note. It is not derived from the function code or compared to the actual function output. As such, the value might be inaccurate.

凄い雑に意訳するとこんな感じででしょうか

  • OutputType属性は関数が返却する.NETオブジェクトの型を一覧 (lists) で、ParameterSetNameパラメーターを使うことができるよ
  • OutputType属性はシンプルな関数と高度な関数 (advanced functions) で使えてCmdletBinding属性とは独立しているよ
  • OutputType属性はGet-Commandコマンドレットの返すSystem.Management.Automation.FunctionInfoOutputTypeプロパティの値になるよ
  • OutputType属性はドキュメンテーションノートだけで、実際の関数の出力は違うかもだ

本文中にもサンプルコードがいくつか示されていますので、それを参考に自分でもコードを書いてみて確認してみます (あくまで引数と戻り値のセットを確認するサンプルコードなので中身はアレですが気にしないでください。あとSwitch内でReturnしていてもBreakを書くのは個人的なコーディング規約です)

Function Get-UserInfo{
    [CmdletBinding(DefaultParameterSetName='Id')]
    [OutputType([String], ParameterSetName='Id')]
    [OutputType([Int], ParameterSetName='Name')]
    param(
        [Parameter(ParameterSetName='Id',Position=0)]
        [Int] $Id,
        [Parameter(ParameterSetName='Name', Position=0)]
        [String] $Name
    )
    Switch ($PsCmdlet.ParameterSetName)
    {
        'Id'{Return 'UserName'; Break}
        'Name'{Return 123456; Break}
    }
}

PS> Get-UserInfo 'UserName'
123456
PS> Get-UserInfo 123456
UserName
PS> Get-Command -Name Get-UserInfo | %{$_.OutputType};
Name          Type          TypeDefinitionAst
----          ----          -----------------
System.String System.String
System.Int32  System.Int32

とりあえず上記のサンプルコードを実行してもりあえずエラーにならず、FunctionInfoのOutputTypeに値が設定されることは分かりました

PS> &([ScriptBlock]{[OutputType([Int])]param()'A';});
A

The OutputType attribute value is only a documentation note. It is not derived from the function code or compared to the actual function output. As such, the value might be inaccurate.とある通り、[OutputType([Int])]としているScriptBlockがString型の 'A' を返却しても、エラーも警告も出ません

OutputType属性で指定した型と異なる出力があったらそこでエラー起こして欲しいところなのですが、それはそれとして、ドキュメント通りのようです


個人的にはOutputType属性は書かないよりは書いておいた方が良いかな、でも、今まで書いてきたコードをわざわざ修正して回るほどでもないな、という認識になりました

Windows PowerShellのFunction入門 (出力編)

この記事はWindows PowerShellのFunctionの出力に関する基本的な話です

PowerShellではReturnがなくても戻り値が返される (むしろ意識的に消さないと意図せぬ戻り値が生じてしまう) という話です


PowerShellではFunction (ScriptBlock/Filter/スクリプトファイル) 内で明示的に出力の指定をしなくても、値が出力されます

そしてコンソールへの出力は、Write-Hostで明示しなくてもコンソール上に表示されます

結果としてコンソール上で実行された関数内に値が書かれていた場合、コンソール上に値が表示されます

PS> Function SampleFunction1 {'Hello World';}
PS> SampleFunction1;
Hello World

複数の出力があった場合、出力はObjectの配列になります

明示的に値を出力したい場合、Write-Outputを使うことが出来ます

Returnを使った場合、Returnに渡された値を出力した上でその関数を終了させ、もし値を渡さなかった場合は単にその関数が終了します

以下のScriptは全て同じ結果になります

Function SampleFunction2 {
    'Hello', 'World';
}

Function SampleFunction3 {
    'Hello';
    'World';
}

Function SampleFunction4 {
    Write-OutPut -InputObject 'Hello', 'World'; 
}

Function SampleFunction5 {
    Write-OutPut -InputObject 'Hello'; 
    Write-OutPut 'World'; # -InputObject は省略可能
}

Function SampleFunction6 {
    Return 'Hello', 'World';
}

Function SampleFunction7 {
    'Hello';
    'World';
    Return;
}

(私が) うっかりしがちな例として、オブジェクトのメソッドの処理し忘れなどで意図しない出力が発生することがあります

Function SampleFunction8 {
    # XxxObject.Setは、処理後のXxx.Objectが返却されるmethod
    XxxObject.Set(0); 
    XxxObject.Set(1); 
    XxxObject.Set(2); 
    XxxObject.Set(3); 
    Return XxxObject.Value; # String型の値のProperty
}

[String] $XxxString = SampleFunction6; # XxxObject.Set() と XxxObject.Value、合計5つの出力を配列で受け取り、エラーとなる

このような場合、何らかの方法で値を捨てる必要があります

Function SampleFunction9 {
    $null = XxxObject.Set(0); # $Nullに代入 (結果、何も出力されないし、$Nullは$Nullのまま)
    [Void] XxxObject.Set(1); # Voidに変換 (結果、何も出力されない)
    XxxObject.Set(2) > $Null; # $Nullに対して出力 (結果、何も出力されない)
    XxxObject.Set(3) | Out-Null; # パイプラインを経由してOut-Nullに値を渡す (結果、何も出力されない)
    Return XxxObject.Value;
}

[String] $XxxString = SampleFunction6; # XxxObject.Valueの結果のみを受け取る

なお、上記で4つの例を示しましたが、PowerShellでNull破棄する際に最も適したやり方を探る - tech.guitarrapc.cómによればOut-Nullだけ実行速度が遅いようなので、 Out-Nullは使わない方が無難なようです

Windows PowerShellのFunction入門 (Begin、Process、Endブロック編)

この記事はWindows PowerShellBeginProcessEndブロック、そしてFunction/Filter/ScriptBlockスクリプトファイルの違いに関する基本的な話です


PowerShellの処理はBeginProcessEndの3つのブロックに分かれています

# Sample1.ps1の内容
Begin{
    Write-Host 'Begin';
}
Process{
    Write-Host 'Process';
}
End{
    Write-Host 'End';
}
PS> ./Sample.ps1;
Begin
Process
End

パイプラインから値を渡されたとき、Beginは最初の1回目のProcessが動く前に、Processは値を渡される毎に、Endは最後のProcessが動いた後に実行されます

なお、パイプラインで値を渡す意図がある場合でもForEach-Objectの中で呼ぶとForEach-Objectの中で個別に実行されてしまうので、注意してください

Function SampleFunction1{
    Begin{
        Write-Host 'Begin';
    }
    Process{
        Write-Host 'Process';
    }
    End{
        Write-Host 'End';
    }
}

PS> 1..3 | SampleFunction1
Begin
Process
Process
Process
End
PS> 1..3 | Foreach-Object{SampleFunction1}
Begin
Process
End
Begin
Process
End
Begin
Process
End

BeginProcessEndブロックの記述はそれぞれ省略することができます

何も書かれていない場合、FilterスクリプトファイルはProcessブロックとして、FunctionScriptBlockEndブロックとして処理が動きます

# Sample2.ps1の内容
    Write-Host 'Sample2.sp1';
PS> 1..3 | ./Sample2.ps1
Sample2.sp1
Sample2.sp1
Sample2.sp1
Function SampleFunction2{
    Write-Host 'SampleFunction2';
}

[ScriptBlock] $ScriptBlock1 = {Write-Host 'ScriptBlock1'};

Filter SampleFilter{
    Write-Host 'SampleFilter';
}


PS> 1..3 | SampleFunction2
SampleFunction2
PS> 1..3 | &($ScriptBlock1)
ScriptBlock1
PS> 1..3 | SampleFilter
SampleFilter
SampleFilter
SampleFilter

また、BeginProcessEndブロックを明記することにより、書かなかったブロックが存在しないFunction/Filter/ScriptBlock/スクリプトファイルを作ることもできます

# Endブロックが存在しないFunction
Function SampleFunction3{
    Begin{
        Write-Host 'Begin';
    }
    Process{
        Write-Host 'Process';
    }
}

BeginProcessEndブロックの何れかを記述した場合、デフォルトで省略されていたブロック (FunctionならEndブロック) も明記する必要が生じます

Function SampleFunction4{
    Process{
        Write-Host 'Process';
    }
    # Endブロック内の処理にはならず、Function作成時にエラーになる
    Write-Host 'End';
}

なお、BeginProcessEndブロックをすべて明記した場合、Function/Filterに違いはなくなるようです……が私はよくわかっていません

FunctionFilterGet-Itemで情報を取得すれば、CommandTypeFilterFunctionかで違いが出ますが、処理に違いはないようです

解説記事なのに最後の所がぼやけていて申し訳ありませんが、私は基本的にはFunctionを用い、パイプラインによる処理の連鎖の中で特定のForEach-Objectが大きくなったり繰り返し同じ記述をすることになった場合Filterで処理を外に出すという使い分けをしています

Windows PowerShellのFunction入門 (命名編)

この記事はWindows PowerShellのFunctionの名前に関する基本的な話です

Get-Verb関数や、FunctionもItemとして扱えるという話を聞いて大体記事の内容が予想着く人はこの記事を読む必要はありません


Windows PowerShellの関数はFunctionFilterを使って以下の様に書きます

Function 名前{
    内容;
}

Filter 名前{
    内容;
}

なお、この記事ではFunctionFilterの違いには触れないので以降はFunctionに一本化して話を進めます

PowerShellの関数 (やコマンドレット) には命名規則があり "動詞-名詞" の形をとります

この命名規則を破ってもPowerShellの文法違反にはならず実行は可能ですが、基本的には守るべきです

使用できる動詞も決まっており、Get-Verbで使える動詞とそのグループの一覧を得ることが出来ます

PS> Get-Verb
Verb        Group
----        -----
Add         Common
Clear       Common

(中略)

Unprotect   Security
Use         Other

例えばGet-Verbで得られる一覧にはShowはありますがDisplayはないので、Holle Worldをコンソールに表示する関数は Display-HelloWorld ではなく Show-HelloWorld にすべきです

試しにHello Worldを関数にすると以下の通りです

Function Show-HelloWorld{
    Write-Host "Hello World";
}

その上でPowerShell命名規則によらないコマンド名で関数を実行したい場合、Set-Aliasで関数に別名を与えることが出来ます

PS> Set-Alias -Name 'HelloWorld' -Value 'Show-HelloWorld';
PS> HelloWorld;
Hello World

ちなみに、Windows PowerShelllsdirGet-ChildItemが実行されるのは、予めAliasが設定されているからです

PS> Get-Alias -Definition 'Get-ChildItem';

CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Alias           dir -> Get-ChildItem
Alias           gci -> Get-ChildItem
Alias           ls -> Get-ChildItem

関数は既にある関数やコマンドレットにかぶせて同じ名前で上書きすることが出来ます (多重定義ではありません)

試しにコマンドレットのWrite-Hostを何もしない同名の関数で上書きしてみます

PS> Function Write-Host{}
PS> Write-Host 'HelloWorld'; # FunctionのWrite-Hostが実行され、何も表示されない

これで、Write-Hostを実行してもコンソールに文字が表示されることはありません

……困ったことになりました

この場合は上書き対象がコマンドレットだったのでRemove-Item'Function:Write-Host'を消すことでコマンドレットへのWrite-Hostへの上書きを解くことができます

PS> Function Write-Host{}
PS> Write-Host 'Hello World'; # FunctionのWrite-Hostが実行され、何も表示されない
PS> Remove-Item -Path 'Function:Write-Host';
PS> Write-Host 'Hello World'; # コマンドレットのWrite-Hostが実行される
Hello World

FunctionをFunctionで上書きした場合、消しても元のFunctionは戻ってきません

多数のモジュールなどを導入している場合、名前が被らないように注意する必要があります (前述のとおり、PowerShellの関数は接頭辞となる動詞が限定されているので名前が被りがちです)


Remove-Itemで削除できたことで分かる通り、FuctionもItemとしてPowerShell上では処理されています

つまり、New-ItemやGet-ItemなどのItem操作を行うコマンドレットで関数を作成、取得、変更、削除出来ます

PS> New-Item -Path 'Function:Show-HelloWorld' -Value 'Write-Host "Hello World";';
CommandType     Name                                               Version    Source
-----------     ----                                               -------    ------
Function        Show-HelloWorld

PS> Show-HelloWorld;
Hello World

PS> Set-Item -Path 'Function:Show-HelloWorld' -Value 'Write-Host "Hello World!!!"';
PS> Show-HelloWorld;
Hello World!!!

PS> Rename-Item -Path 'Function:Show-HelloWorld' -NewName 'Show-HelloWorld!!!'
PS> Show-HelloWorld!!!;
Hello World!!!

PS> Set-Content -Path 'Function:Show-HelloWorld!!!' -Value 'Write-Host "Hello World!!!`nHello World!!!`nHello World!!!";';
PS> Show-HelloWorld!!!
Hello World!!!
Hello World!!!
Hello World!!!

割とやりたい放題です

DateTimeとTimeSpanをWindows PowerShellであれこれ

書き始めた時はWindows PowerShellで時刻と時間をあれこれだったのですが、記事を書きあげてみたらWindows PowerShellがあんまり関係ない話になっていました

この記事は DateTime、TimeSpan、TimeZoneInfo、CultureInfoを知っている人には不要です


WindowsPowerShell (というか.NETで) 時間に関するデータを扱う場合、時刻 (特定の日時) ならDiteTimeを、時間 (時刻から時刻の間) ならTimeSpanはを使います

Windows PowerShell でDiteTimeのオブジェクトを作る場合、他のオブジェクト同様にNewからも作れますが、文字列や数値からの変換でも作ることが出来ます

数値を与えた場合、Ticks (1/10,000,000秒、西暦1年1月1日午前0時からの経過時間) として扱われます

PS> [DateTime]::New(2017,1,2,3,4,5)
2017年1月2日 3:04:05

PS> [DateTime]'2017/6/7 8:9:10'
2017年6月7日 8:09:10

PS> [DateTime]10000000
0001年1月1日 0:00:01

TimeSpan型も同様にNewや数値からも作れます (DateTime同様 Ticks として扱われます) が、DateTimeオブジェクト同士を減算するとTimeSpanオブジェクトが戻り値になります

ちなみに、DateTimeオブジェクトにTimeSpanオブジェクトを加算または減算するとDateTimeオブジェクトになります

PS> [TimeSpan]10000000;
Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 1
Milliseconds      : 0
Ticks             : 10000000
TotalDays         : 1.15740740740741E-05
TotalHours        : 0.000277777777777778
TotalMinutes      : 0.0166666666666667
TotalSeconds      : 1
TotalMilliseconds : 1000

PS> [DateTime] '2017/1/1' - [TimeSpan]::New(10,0,0,0);
2016年12月22日 0:00:00

PS> [DateTime]'2017/12/1 12:0:0+05:00' - [DateTime] '2017/1/1 1:10:30-09:00';
Days              : 333
Hours             : 20
Minutes           : 49
Seconds           : 30
Milliseconds      : 0
Ticks             : 288461700000000
TotalDays         : 333.867708333333
TotalHours        : 8012.825
TotalMinutes      : 480769.5
TotalSeconds      : 28846170
TotalMilliseconds : 28846170000

Windows PowerShellでこれらを扱う場合、DateTimeオブジェクトとTimeSpanオブジェクトはそれぞれGet-Dateコマンドレット、New-TimeSpanコマンドレットでも生成できます

Get-Dateコマンドレットはパラメータなしで実行すると現在時刻が設定され、[DateTime]::Nowと同じになります

Get-Dateには.NETのStrings.Formatと同じ形式で出力される形式を指定できる-Formatパラメータ、UNIXのdateのフォーマットと同じ書式で出力される形式を指定できる-UFormatがあります

-Formatの場合、ToStringで同様の処理ができますが、-UFormatを使いたい場合、Get-Dateを使うことになります

PS> ([DateTime] '2017/12/1 15:0:0+09:00').ToString('yyyy年MM月dd日 (dddd) HH時mm分ss秒');
2017年12月01日 (金曜日) 15時00分00秒

PS> Get-Date -Date '2017/12/1 15:0:0+09:00' -Format 'yyyy年MM月dd日 (dddd) HH時mm分ss秒';
2017年12月01日 (金曜日) 15時00分00秒

PS> Get-Date -Date '2017/12/1 15:0:0+09:00' -UFormat '%Y年%m月%d (%A) %H時%M分%S秒'
2017年12月01日 (金曜日) 15時00分00秒

ここまでの例では世界標準時との時差が自動的に9時間に補正されていたり、曜日の表記が日本語になっていますが、これは実行環境に依存した値です

まず、時差を決定しているタイムゾーンの情報については [TimeZoneInfo]::Local で現在のシステムのタイムゾーンを取得できます

時差はBaseUtcOffsetプロパティから取得でき、TimeSpan型です

PS> >[TimeZoneInfo]::Local;
Id                         : Tokyo Standard Time
DisplayName                : (UTC+09:00) 大阪、札幌、東京
StandardName               : 東京 (標準時)
DaylightName               : 東京 (夏時間)
BaseUtcOffset              : 09:00:00
SupportsDaylightSavingTime : False

PS> [System.TimeZoneInfo]::Local.BaseUtcOffset.GetType().Name;
TimeSpan

タイムゾーンIDでDateTimeを別の時間帯の時刻に変換することもできます

タイムゾーンIDは[TimeZoneInfo]::GetSystemTimeZones()により全てのタイムゾーンを取得できるのでそこから調べる事ができます

また、[TimeZoneInfo]::FindSystemTimeZoneByIdを用いて、IdからTimeZoneInfoを得ることもできます

PS> [TimeZoneInfo]::ConvertTimeBySystemTimeZoneId([DateTime]'2017/12/1 15:00:00+09:00', 'Eastern Standard Time');
2017年12月1日 1:00:00

[TimeZoneInfo]::FindSystemTimeZoneById('Eastern Standard Time');
Id                         : Eastern Standard Time
DisplayName                : (UTC-05:00) 東部標準時 (米国およびカナダ)
StandardName               : 東部標準時
DaylightName               : 東部夏時間
BaseUtcOffset              : -05:00:00
SupportsDaylightSavingTime : True

一方、2017/12/1の曜日を金曜日と表示されているのは CultureInfo によるものです

上記の例で2017/12/1の曜日が金曜日と日本語で出力されていたのはが、実行した環境のCultureInfoが日本 ('ja-JP ')だからで、例えば2017/12/1の曜日を "金曜日" でなく 英語 (米国) 表記で "Friday" と得たい場合、'en-US' を設定したカルチャ情報を指定することで実現できます

PS> [CultureInfo]::CurrentCulture;
LCID             Name             DisplayName
----             ----             -----------
1041             ja-JP            日本語 (日本)

PS> [CultureInfo]::new('en-US');
LCID             Name             DisplayName
----             ----             -----------
1033             en-US            英語 (米国)

PS> ([DateTime] '2017/12/1').ToString('dddd', [CultureInfo]::new('en-US'));
Friday

カルチャ情報に結び付けられた日付の表記情報についてはDateTimeFormatプロパティを参照してみてください

そんな感じで一つ

Windows PowerShellでフォルダ内の重複するファイルを調べる

前の記事では(私の想像で) 人間が手作業している日常業務を自動化する例としてWindows PowerShellでExcelファイルを編集すると言う内容を書きましたが、余りPowerShellっぽさがないスクリプトになってしまったので、今度はPowerShellっぽいスクリプトになるようにフォルダ内の重複するファイルを調べてみます


とりあえず今回のサンプルコードとしてはピクチャフォルダ配下のファイルを調べることにします

まずはファイルの一覧を取ってみましょう

Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force

はい、取れました

ピクチャフォルダを[Environment]::GetFolderPath('MyPictures')で取得してGet-ChildItem-Pathに設定し、ファイルだけ欲しいので-File付けて、子アイテムがフォルダの場合その中身も再帰的に取得するよう-Recurse付けて、ついでに隠しファイルも取れるよう-Forceも付けました

いままでDOSでバッチを書いていた身からすると、PowerShellは本当に強力です

次に同一ファイルを特定する情報の取得ですが、今回はHashを使う事にします

Hashが何かについてはこの記事では説明しないので、各自ハッシュ関数で検索して下さい

Windows PowerShellではversion 4から標準コマンドレットにGet-FileHashと言う、ファイルハッシュを取得するその名の通りのコマンドレットが追加されていますので、先ほど取得したファイルリストをパイプ (|) を使ってGet-FileHashに流し込んでみます

Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force |
    Get-FileHash -Algorithm SHA256;

はい、できました

Get-FileHashはパイプラインから流れてくるSystem.IO.FileSystemInfoを直接受け取れるので上記のように書けます

ちなみに、-AlgorithmパラメータにSHA256を与えていますがここはパフォーマンスと相談して好きなアルゴリズムを採用してください (今回のサンプルコードでは悪意のある第三者が改竄したファイルでhashを衝突させてくる心配はほぼないですし)

そして、ユニークではないhashを持つObjectだけ出力したいので、とりあえず今回はSort-Object -Property HashでHashでソートした上で直前のObjectのhashと同じかどうかを判定することにします

[String] $prevHash = '';
[PSCustomObject] $prevObject = $null;
Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force |
    Get-FileHash -Algorithm SHA256 |
    Sort-Object -Property Hash |
    Foreach-Object {If($_.Hash -eq $prevHash){$prevObject; $_;} $PrevObject=$_;$PrevHash=$_.Hash;}

しかし、これだと一致するファイル A1,A2,A3 があった場合、A1,A2,A2,A3 と、A2が2回出力されてしまいます

そこで、その後Select-Object -Property * -Uniqueとして重複したObjectを取り除くことにします

[String] $prevHash = '';
[PSCustomObject] $prevObject = $null;
Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force |
    Get-FileHash -Algorithm SHA256 |
    Sort-Object -Property Hash |
    Foreach-Object {If($_.Hash -eq $prevHash){$prevObject; $_;} $PrevObject=$_;$PrevHash=$_.Hash;} |
    Select-Object -Property * -Unique;

以上でピクチャフォルダ配下の重複したファイルが分かるようになりましたが、重複はしているがそれぞれ必要なファイルの場合もありますので、削除や移動はせず、CSVファイルに出力することにします

CSVに出力する場合、Export-CSVと言うそのままの名前のコマンドレットを使います

[String] $prevHash = '';
[PSCustomObject] $prevObject = $null;
[String] $CsvFullName = Join-Path -Path ([Environment]::GetFolderPath('Desktop')) -CHildPath '重複ファイルリスト.csv'
Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force |
    Get-FileHash -Algorithm SHA256 |
    Sort-Object -Property Hash |
    Foreach-Object {If($_.Hash -eq $prevHash){$prevObject; $_;} $PrevObject=$_;$PrevHash=$_.Hash;} |
    Select-Object -Property * -Unique |
    Export-CSV -Path $CsvFullName -Encoding Default -Force -NoTypeInformation;

ほぼ完成です

ですが、出力されたCSVを開いてみると先頭の列はAlgorithmという、今回は全行同じ値が必ず入っているので要らない情報が有ったり、逆に (同じファイルとはいえ) 最終更新日等があった方がどのファイルを消すかの参考情報になって便利などの微妙に使い勝手が悪いことが気になります

そこで、Get-FileHashの行を書き換え、素のGet-FileHashの出力結果ではなく、Select-Objectを使って必要な情報を持ったPSCustomObjectを次のパイプに渡すように変更してみます

[String] $prevHash = '';
[PSCustomObject] $prevObject = $null;
[String] $CsvFullName = Join-Path -Path ([Environment]::GetFolderPath('Desktop')) -CHildPath '重複ファイルリスト.csv'
Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force |
    Select-Object -Property Name,Directory,FullName,Length,Extension,LastWriteTime,CreationTime,@{Name='Hash';Expression={(Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash}} |
    Sort-Object -Property Hash |
    Foreach-Object {If($_.Hash -eq $prevHash){$prevObject; $_;} $PrevObject=$_;$PrevHash=$_.Hash;} |
    Select-Object -Property * -Unique |
    Export-CSV -Path $CsvFullName -Encoding Default -Force -NoTypeInformation;

最後に、このコードをps1ファイルに保存するときにはファイルの先頭に#Requires -Version 4.0と実行時の最低バージョンの指定を書いておくと良いでしょう

今回のスクリプトではversion 4.0から追加されたGet-FileHashを使っていますが、2017年12月現在、Version 4.0が入っていない環境は一応残っています (参考: Windows PowerShell のシステム要件 | Microsoft Docs)

そんな訳で最終的にはこんなバッチファイルになりました

#Requires -Version 4.0

[String] $prevHash = '';
[PSCustomObject] $prevObject = $null;
[String] $CsvFullName = Join-Path -Path ([Environment]::GetFolderPath('Desktop')) -CHildPath '重複ファイルリスト.csv'
Get-ChildItem -Path ([Environment]::GetFolderPath('MyPictures')) -File -Recurse -Force |
    Select-Object -Property Name,Directory,FullName,Length,Extension,LastWriteTime,CreationTime,@{Name='Hash';Expression={(Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash}} |
    Sort-Object -Property Hash |
    Foreach-Object {If($_.Hash -eq $prevHash){$prevObject; $_;} $PrevObject=$_;$PrevHash=$_.Hash;} |
    Select-Object -Property * -Unique |
    Export-CSV -Path $CsvFullName -Encoding Default -Force -NoTypeInformation;

パイプをどんどん繋げてObjectを流していると、俺、シェル書いてるって気分になって楽しいです