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.
$input
はBegin
ブロックでも読み出せますが値は入っておらず、何も入っていません
# 値は読み出せないが存在はしているので 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.FunctionInfo
のOutputType
プロパティの値になるよ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 PowerShellのBegin
、Process
、End
ブロック、そしてFunction
/Filter
/ScriptBlock
スクリプトファイルの違いに関する基本的な話です
PowerShellの処理はBegin
、Process
、End
の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
Begin
、Process
、End
ブロックの記述はそれぞれ省略することができます
何も書かれていない場合、Filter
とスクリプトファイルはProcess
ブロックとして、Function
とScriptBlock
はEnd
ブロックとして処理が動きます
# 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
また、Begin
、Process
、End
ブロックを明記することにより、書かなかったブロックが存在しないFunction
/Filter
/ScriptBlock
/スクリプトファイルを作ることもできます
# Endブロックが存在しないFunction Function SampleFunction3{ Begin{ Write-Host 'Begin'; } Process{ Write-Host 'Process'; } }
Begin
、Process
、End
ブロックの何れかを記述した場合、デフォルトで省略されていたブロック (Function
ならEnd
ブロック) も明記する必要が生じます
Function SampleFunction4{ Process{ Write-Host 'Process'; } # Endブロック内の処理にはならず、Function作成時にエラーになる Write-Host 'End'; }
なお、Begin
、Process
、End
ブロックをすべて明記した場合、Function
/Filter
に違いはなくなるようです……が私はよくわかっていません
Function
とFilter
はGet-Item
で情報を取得すれば、CommandType
がFilter
かFunction
かで違いが出ますが、処理に違いはないようです
解説記事なのに最後の所がぼやけていて申し訳ありませんが、私は基本的にはFunctionを用い、パイプラインによる処理の連鎖の中で特定のForEach-Objectが大きくなったり繰り返し同じ記述をすることになった場合Filterで処理を外に出すという使い分けをしています
Windows PowerShellのFunction入門 (命名編)
この記事はWindows PowerShellのFunctionの名前に関する基本的な話です
Get-Verb
関数や、FunctionもItemとして扱えるという話を聞いて大体記事の内容が予想着く人はこの記事を読む必要はありません
Windows PowerShellの関数はFunction
かFilter
を使って以下の様に書きます
Function 名前{ 内容; } Filter 名前{ 内容; }
なお、この記事ではFunction
とFilter
の違いには触れないので以降は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 PowerShellでls
やdir
でGet-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を知っている人には不要です
- DateTime 構造体 (System).aspx)
- TimeSpan 構造体 (System).aspx)
- TimeZoneInfo クラス (System).aspx)
- CultureInfo クラス (System.Globalization).aspx)
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の曜日を
上記の例で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を流していると、俺、シェル書いてるって気分になって楽しいです